注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Android组件化第一步壳工程配置

传统项目开发中,我们都是通过集成化的方式来搭建项目的架构。什么叫做集成化,我的理解,就是整个project有一个module,根据功能的需要来创建不同的library库,通过gradle的方式来实现依赖。 什么叫做组件化,我的理解就是,一个project中,将...
继续阅读 »

传统项目开发中,我们都是通过集成化的方式来搭建项目的架构。什么叫做集成化,我的理解,就是整个project有一个module,根据功能的需要来创建不同的library库,通过gradle的方式来实现依赖。


什么叫做组件化,我的理解就是,一个project中,将会有多个module,并且这个module可以在需要的时候切换身份,变成library,作为主module的依赖,主 module 就是我们的壳工程。


为什么会想尝试一下组件化呢?有两个愿景:


1.在开发中,可以不用打包整个app。实现测试同学的测试包和开发人员的自测包分离


2.减少开发自测时的打包时间。


以上就是我对组件化目前的理解,对于路由那块,分篇讨论。为了解决上面愿景中的两个问题,我们可以做如下配置。


开始步骤1,2,3...

1.在项目的build.gradle中创建一个boolean变量,用来确定编译项目为集成化模式还是组件化模式。同时配置buildconfig。方便在代码中进行判断处理。


image2021-3-5_9-28-45.png


这儿说一下,gradle的引入机制,没有根据文档,只是主观推断。gradle会先从项目的build.gradle中进行读取,通过ext来定义整个工程的变量,通过apply from 来引入其他的gradle配置文件,在project中配置的功能和变量,将会在整个工程中都可以使用。


2.在要做成组件化的library中进行配置,主要是切换plugin是library还是module,以及是否在default中展示application Id,这儿有可能因为依赖的库太多,需要配置mutidex,来解决65535的问题。def用于定义子module内部的变量。


image2021-3-5_9-29-7.png


image2021-3-5_9-29-18.png


3.在依赖该library的地方,也就是主module地方,进行配置。如果是集成化的配置,也就是isRelease为true,才可以依赖,否则会在编译时产生依赖重复引入异常,无法编译通过。同时在defaultConfig里面配置buildconfig变量,方便代码中使用,进行功能切换


image2021-3-5_9-29-35.png


// 如果是集成化模式,做发布版本时。各个模块都不能独立运行了

if (isRelease) {

implementation project(':YoungWear')

}

4.配置两个AndroidManifest,作为module时候是有Application的,同时按照mutidex的配置方案配置module,接下来是一些核心的代码配置

// 配置资源路径,方便测试环境,打包不集成到正式环境

sourceSets {

main {

if (!isRelease) {

// 如果是组件化模式,需要单独运行时

manifest.srcFile 'src/main/debug/AndroidManifest.xml'

} else {

// 集成化模式,整个项目打包apk

manifest.srcFile 'src/main/AndroidManifest.xml'

java {

// release 时 debug 目录下文件不需要合并到主工程

exclude '**/debug/**'

}}}}

image2021-3-5_9-30-16.png


windows下,分别给project里的build.gradle赋值true和false,terminal中输入gradlew compileDebugSource --stacktrace -info ,查看是否可以编译成功,当作为module的方式,可以在AS中,看到如下图的图标正常,就证明配置成功了,直接安装apk到手机就可以了。


image2021-3-5_9-30-36.png


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

Android开发:实现滑动退出 Fragment + Activity 二合一

前言 能否在不包含侧滑菜单的时候,添加一个侧滑返回,边缘finish当前Fragment? 今天把这项工作完成了,做成了单独的SwipeBackFragment库以及Fragmentation-SwipeBack拓展库 特性: 1、SwipeBackFra...
继续阅读 »

前言



能否在不包含侧滑菜单的时候,添加一个侧滑返回,边缘finish当前Fragment?



今天把这项工作完成了,做成了单独的SwipeBackFragment库以及Fragmentation-SwipeBack拓展库


特性:

1、SwipeBackFragment , SwipeBackActivity二合一:当Activity内的Fragment数大于1时,滑动finish的是Fragment,如果小于等于1时,finish的是Activity。


2、支持左、右、左&右滑动(未来可能会增加更多滑动区域)


3、支持Scroll中的滑动监听


4、帮你处理了app被系统强杀后引起的Fragment重叠的情况


效果



效果图


谈谈实现


拖拽部分大部分是靠ViewDragHelper来实现的,ViewDragHelper帮我们处理了大量Touch相关事件,以及对速度、释放后的一些逻辑监控,大大简化了我们对触摸事件的处理。(本篇不对ViewDragHelper做详细介绍,有不熟悉的小伙伴可以自行查阅相关文档)


对Fragment以及Activiy的滑动退出,原理是一样的,都是在Activity/Fragment的视图上,添加一个父View:SwipeBackLayout,该Layout里创建ViewDragHelper,控制Activity/Fragment视图的拖拽。


1、Activity的实现


对于Activity的SwipeBack实现,网上有大量分析,这里我简要介绍下原理,如下图:



我们只要保证SwipeBackLayout、DecorView和Window的背景是透明的,这样拖拽Activity的xml布局时,可以看到上个Activity的界面,把布局滑走时,再finish掉该Activity即可。


public void attachToActivity(FragmentActivity activity) {
...
ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView();
ViewGroup decorChild = (ViewGroup) decor.getChildAt(0);
decorChild.setBackgroundResource(background);
decor.removeView(decorChild); // 移除decorChild
addView(decorChild); // 添加decorChild到SwipeBackLayout(FrameLayout)
setContentView(decorChild);
decor.addView(this);} // 把SwipeBackLayout添加到DecorView下


2、Fragment的实现


重点来了,Fragment的实现!

在实现前,我先说明Fragment的几个相关知识点:


1、Fragment的视图部分其实就是在onCreateView返回的View;


2、同一个Activity里的多个通过add装载的Fragment,他们在视图层是叠加上去的:

hide()并不销毁视图,仅仅让视图不可见,即View.setVisibility(GONE);

show()让视图变为可见,即View.setVisibility(VISIBLE);



add+show/hide的情况


3、通过replace装载的Fragment,他们在视图层是替换的,replace()会销毁当前的Fragment视图,即回调onDestoryView,返回时,重新创建视图,即回调onCreateView;



replace的情况


4、不管add还是replace,Fragment对象都会被FragmentManager保存在内存中,即使app在后台因系统资源不足被强杀,FragmentManager也会为你保存Fragment,当重启app时,我们可以从FragmentManager中获取这些Fragment。


分析:


Fragment之间的启动无非下图中的2种:



而这个库我并没有考虑replace的情况,因为我们的SwipeBackFragment应该是在"流式"使用的场景(FragmentA -> FragmentB ->....),而这种场景下结合上面的2、3、4条,add+show(),hide()无疑更优于replace,性能更佳、响应更快、我们app的代码逻辑更简单。


add+hide的方式的实现


从第1条,我们可以知道onCreateView的View就是需要放入SwipeBackLayout的子View,我们给该子View一个背景色,然后SwipeBackLayout透明,这样在拖拽时,即可看到"上个Fragment"。


当我们拖拽时,上个Fragment A的View是GONE状态,所以我们要做的就是当判断拖拽发生时,Fragment A的View设置为VISIBLE状态,这样拖拽的时候,上个Fragment A就被完好的显示出来了。


核心代码:


@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(...);
return attachToSwipeBack(view);
}

protected View attachToSwipeBack(View view) {
mSwipeBackLayout.addView(view);
mSwipeBackLayout.setFragment(this, view);
return mSwipeBackLayout;
}


但是相比Activity,上个Activity的视图状态是VISIBLE的,而我们的上个Fragment的视图状态是GONE的,所以我们需要FragmentA.getView().setVisibility(VISIBLE),但是时机是什么时候呢?


最好的方案是开始拖拽前的那一刻,我是在ViewDragHelper里的tryCaptureView方法处理的:


@Override
public boolean tryCaptureView(View child, int pointerId) {
boolean dragEnable = mHelper.isEdgeTouched(ViewDragHelper.EDGE_LEFT);
if (mPreFragment == null) {
if (dragEnable && mFragment != null) {
...省略获取上一个Fragment代码
mPreFragment = fragment;
mPreFragment.getView().setVisibility(VISIBLE);
break;
}
} else {
View preView = mPreFragment.getView();
if (preView != null && preView.getVisibility() != VISIBLE) {
preView.setVisibility(VISIBLE);
}
}
return dragEnable;
}


通过上面代码,我们拖拽当前Fragment前的一瞬间,PreFragment的视图会被VISIBLE,同时完全不会影响onHiddenChanged方法,完美。(到这之前可能有小伙伴想到,只通过add不hide上个Fragment的思路怎么样?很明显是不行的,因为这样的话onHiddenChanged方法不会被回调,而我们使用add的方式,主要通过onHiddenChanged来作为“生命周期”来实现我们的逻辑的)


还一种情况需要注意,当我已经开始拖拽FragmentB打算pop时,拖拽到一半我放弃了,这时FragmentA的视图已经是VISIBLE状态,我又从B进入到Fragment C,这是我们应该把A的视图GONE掉:


SwipeBackFragment里:
@Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
if (hidden && mSwipeBackLayout != null) {
mSwipeBackLayout.hiddenFragment();
}
}

SwipeBackLayout里:
public void hiddenFragment() {
if (mPreFragment != null && mPreFragment.getView() != null) {
mPreFragment.getView().setVisibility(GONE);
}
}


坑点


1、触摸事件冲突


当我们所拖拽的边缘区域中的子View,有其他Touch事件,比如Click事件,这时我们会发现我们的拖拽失效了,这是因为,如果子View不消耗事件,那么整个Touch流程直接走onTouchEvent,在onTouchEvent的DOWN的时候就确定了CaptureView。如果子View消耗事件,那么就会先走onInterceptTouchEvent方法,判断是否可以捕获,而在这过程中会去判断另外两个回调的方法:getViewHorizontalDragRange和getViewVerticalDragRange,只有这两个方法返回大于0的值才能正常的捕获;


并且你需要考虑当前拖拽的页面下是有2个SwipeBackLayout:当前Fragment的和Activity的,最后代码如下:


@Override
public int getViewHorizontalDragRange(View child) {
if (mFragment != null) {
return 1;
} else {
if (mActivity != null && mActivity.getSupportFragmentManager().getBackStackEntryCount() == 1) {
return 1;
}
}
return 0;
}


这样的话,一方面解决了事件冲突,一方面完成了Activity内Fragment数量大于1时,拖拽的是Fragment,等于1时拖拽的是Activity。


2、动画


我们需要在拖拽完成时,将Fragment/Activity移出屏幕,紧接着关闭,最重要的是要保证当前Fragment/Actiivty关闭和上一个Fragment/Activity进入时是无动画的!


对于Activity这项工作很简单:Activity.overridePendingTransition(0, 0)即可。


对于Fragment,如果本身在Fragment跳转时,就不为其设置转场动画,那就可以直接使用了;

如果你使用了setCustomAnimations(enter,exit)或者setCustomAnimations(enter,exit,popenter,popexit),你可以这样处理:


SwipeBackLayout里:
{
mPreFragment.mLocking = true;
mFragment.mLocking =true;
mFragment.getFragmentManager().popBackStackImmediate();
mFragment.mLocking = false;
mPreFragment.mLocking = false;
}

SwipeBackFragment里:
@Override
public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
if(mLocking){
return mNoAnim;
}
return super.onCreateAnimation(transit, enter, nextAnim);
}


3、启动新Fragment时,不要调用show()


getSupportFragmentManager().beginTransaction()
.setCustomAnimations(xxx)
.add(xx, B)
// .show(B)
.hide(A)
.commit();


请不要调用上述代码里的show(B)

一方面是新add的B本身就是可见状态,不管你是show还是不调用show,都不会回调B的onHiddenChanged方法;

另一方面,如果你调用了show,滑动返回会后出现异常行为,回到PreFragment时,PreFragment的视图会是GONE状态;如果你非要调用show的话,请按下面的方式处理:(没必要的话,还是不要调用show了,下面的代码可能会产生闪烁)


@Overridepublic void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
if (!hidden && getView().getVisibility() != View.VISIBLE) {
getView().post(new Runnable() {
@Override
public void run() {
getView().setVisibility(View.VISIBLE);
}
});
}
}


最后


我为什么把这个库做成2个,一个单独使用的SwipeBackFragment和一个Fragmentation-SwipeBack拓展库呢?


原因在于:

SwipeBackFragment库是一个仅实现Fragment&Activity拖拽返回的基础库,适合轻度使用Fragment的小伙伴(项目属于多Activity+多Fragment,Fragment之间没有复杂的逻辑),当然你也可以随意拓展。


Fragmentation主要是在项目结构为 单Activity+多Fragment,或者重度使用Fragment的多Activity+多Fragment结构时的一个Fragment帮助库,Fragment-SwipeBack是在其基础上拓展的一个库,用于实现滑动返回功能,可以用于各种项目结构。


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

HashMap源码解析

带着问题看HashMap源码(基于JDK8) HashMap由于涉及到多个数据结构,所以变成了面试题的常客,下面带着以下几个面试常见问题去阅读JDK8中HashMap的源码 HashMap底层数据结构 HashMap的put过程 HashMap的get过程...
继续阅读 »

带着问题看HashMap源码(基于JDK8)



  • HashMap由于涉及到多个数据结构,所以变成了面试题的常客,下面带着以下几个面试常见问题去阅读JDK8中HashMap的源码

    1. HashMap底层数据结构

    2. HashMap的put过程

    3. HashMap的get过程

    4. HashMap如何扩容,扩容为啥是之前的2倍

    5. HashMap在JDK8中为啥要改成尾插法




1、HashMap底层数据结构



  • HashMap的数据结构是数组 + 链表 + 红黑树

    • 默认是存储的Node节点的数组


    Node<K,V>[] table;

    static class Node<K,V> implements Map.Entry<K,V> {
    final int hash; // 存储的key的hash值
    final K key; // key键
    V value; // value值
    Node<K,V> next; // 链表指向的下一个节点


    • 当Node节点中链表(next)长度超过8时会将链表转换为红黑树TreeNode(Node的子类)以提高查询效率


    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent; // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev; // needed to unlink next upon deletion
    boolean red;


  • Node[]数组的初始长度默认为16,并且必须为2^n的形式(具体原因下面会有解释)


/**
* The default initial capacity - MUST be a power of two.
* 默认初始容量为16,并且必须为2的幂数
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16


  • HashMap默认的阈值threshold = 负载因子loadFactor(默认为0.75)*容量capacity,即初始时默认为16 * 0.75 = 12

    • 表示当hashMap中存储的元素超过该阈值时,为了减少hash碰撞,会对hashMap的容量Capacity进行resize扩容,每次扩容都是之前的2倍,扩容后会重新计算hash值即重新计算在新的存放位置并插入


    /**
    * The load factor used when none specified in constructor.
    * 当没有在构造中指定loadFactor加载因子时,默认值为0.75
    */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;



2、HashMap的put过程


put & putIfAbsent


/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
* 将指定的值与此映射中的指定键相关联。如果映射以前包含键的映射,则旧的值被替换
*
* @param key key with which the specified value is to be associated key值
* @param value value to be associated with the specified key key对应的Value值
* @return the previous value associated with key, or null if there was no mapping for key
* 当hashmap中已有当前key覆盖更新并返回旧的Value,如果没有返回null
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

// onlyIfAbsent参数为true,表示仅在不包含该key时会插入,已包含要插入的key时则不会覆盖更新
@Override
public V putIfAbsent(K key, V value) {
return putVal(hash(key), key, value, true, true);
}

hash方法计算key的hash值


// 通过key计算hash值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

putVal相关代码


/**
* Implements Map.put and related methods
*
* @param hash hash for key key的hash值,通过hash方法获取
* @param key the key 键
* @param value the value to put 值
* @param onlyIfAbsent if true, don't change existing value 当已有key时是否覆盖更新
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none 返回旧的值,如果没有相同的key返回null
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 1、第一次put时table为null,就会触发resize,将初始化工作延迟到第一次添加元素时,懒加载
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2、将hash值与size-1进行&运算得出数组存放的位置;当此位置上还未存放Node时
// 直接初始化创建一个Node(hash,key,value,null)并放置在该位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;e
// 3、假如该位置已经有值,但存储的key完全相同时,直接将原来的值赋值给临时e
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 4、假如该位置有值,key值也不同,先判断该Node是不是一个TreeNode类型(红黑树,Node的子类)
// 就调用putTreeVal方法执行红黑树的插入操作
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 5、假如该位置有值,key值也不同,Node也不是一个TreeNode红黑树类型,
// 便会对链表进行遍历并对链表长度进行计数,遍历到链表中有相同key的节点会跳出遍历
// 当链表长度计数的值超过8(包含数组本身上的Node)时
// 就会触发treeifyBin操作即将链表转化为红黑树
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 这里主要针对相同的key做处理,当onlyIfAbsent为true时就不覆盖,为false时覆盖更新
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 6、当hashMap存储的元素数量超过阈值就会触发resize扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

resize扩容相关代码


/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;、
// 针对后续扩容
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 这里针对构造器中自行设置了initialCapacity的情况
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 针对第一次put时,Node数组相关参数初始化
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 链表数组初始化
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 扩容时将旧的Node移到新的数组操作
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 判断高位是1还是0
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

put大致的流程总结



  1. 第一次put元素时会触发resize方法,其实是将hashMap的Node[]数组初始化工作进行了类似懒加载的处理

  2. 将hash值与capacity-1进行&运算计算出当前key要放置在数组中的位置;当该位置无值时就会直接初始化创建一个Node(hash,key,value,null)并放置在该位置,如果已有值就先判断存储和插入的key是否相等,相等的话通过onlyIfAbsent参数判定是否要覆盖更新并返回旧值

  3. 如果已有值并且与要存储的key不等,就先判定该Node是否是一个TreeNode(红黑树,Node的子类),是的话就调用putTreeVal方法执行红黑树的插入操作

  4. 如果已有值并且与要存储的key不等也不是一个红黑树节点TreeNode就会对Node链表进行遍历操作,遍历到链表中有相同key就跳出根据onlyIfAbsent参数判定是否要覆盖更新,如果没有便新建Node,放置在Node链表的Next位置;如果链表长度超过8时便会将链表转化为红黑树并重新插入

  5. 最后判断HashMap存储的元素是否超过了阈值,超过阈值便会执行resize扩容操作,并且每次扩容都是之前的2倍。扩容后重新进行hash&(capacity-1)计算元素的插入位置重新插入


image.png


3、HashMap的get过程


get方法执行



  • 实质上是调用的getNode方法


public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}

getNode方法


/**
* Implements Map.get and related methods
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 先判断Node数组是否为空或length为0或是否存储的值本身为null,如果是直接返回null
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 当匹配到节点数组上的Node的hash和key都相同时直接返回该Node
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
// 判断Node.next,如果为TreeNode红黑树类型就利用getTreeNode方法进行红黑树的查找
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
// 不是红黑树结构就是链表结构,进行链表遍历操作,直至找到链表中hash和key值都相等
// 的元素便返回该Node
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}

get大致的流程总结



  1. get方法实质调用的是getNode方法

  2. 首先通过hash(key)方法计算出key的hash值,再通过hash&(capacity-1)计算出要查找的Node数组中的元素位置

  3. 假如Node数组为null或者数组length为0或者该位置本身存储的元素就是null就直接返回null

  4. 假如该位置存储的元素不为null,直接对该位置的Node的hash和key进行匹配,假如都相等便匹配成功返回该Node

  5. 假如该数组上的Node不匹配就获取该Node的next元素,首先判断该元素是否是一个TreeNode红黑树节点类型的Node,如果是就利用getTreeNode方法进行红黑树的查找,找到返回该节点,找不到返回null

  6. 如果next节点的Node不是TreeNode表明是一个链表结构,直接循环遍历该链表,直至找到该值,或最后一个链表元素仍然不匹配就跳出循环返回null


4、HashMap如何扩容,扩容为啥是之前的2倍



  • HashMap中当存储的元素数量超过阈值时就会触发扩容,每次扩容后容量会变成之前的2倍

  • 因为扩容为2倍时,capacity-1转换成2进制后每一位都为1,使得hash&(capacity-1)计算得出要存放的新位置要么是之前的位置要么是之前的位置+ 之前的capacity,使得在扩容时,不需要重新计算元素的hash了,只需要判断最高位是1还是0就好了(hash&oldCapacity),一方面降低了hash冲突,一方面提升了扩容后重新插入的效率


image.png


5、HashMap在JDK8中为啥要改成尾插法



  • 参考:juejin.cn/post/684490…

  • HashMap在jdk1.7中采用头插入法,在扩容时会改变链表中元素原本的顺序,以至于在并发场景下导致链表成环的问题。而在jdk1.8中采用尾插入法,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了

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

Swift-Router 自己写个路由吧,第三方总是太复杂

iOS
Swift-Router 自己写个路由吧,第三方总是太复杂先看看这个路由的使用吧如果是网络地址,会直接自动跳转到 OtherWKWebViewController如果是应用内部的手动调用跳转直接跳转视图控制器EPRouter.pushViewControlle...
继续阅读 »

Swift-Router 自己写个路由吧,第三方总是太复杂

先看看这个路由的使用吧
  1. 如果是网络地址,会直接自动跳转到 OtherWKWebViewController
  2. 如果是应用内部的手动调用跳转
  • 直接跳转视图控制器
    • EPRouter.pushViewController(EPSMSLoginViewController())
  • 先在 RouteDict 注册映射关系再跳转
    • EPRouter.pushAppURLPath("goods/detail?spellId=xxx&productId=xxx")
  1. 又服务器来控制跳转 也得在 RouteDict 注册映射关系,只不过多加了一个 scheme
    • EPRouter.pushURLPath("applicationScheme://goods/detail?spellId=xxx&productId=xxx")

**!!!支持Swift、OC、Storyboard的跳转方式,可以在 loadViewController 看到实现方式 **

EPRouter的全部代码
class EPRouter: NSObject {

    private static let RouteDict:[String:String] = [
        "order/list"            :"OrderListPageViewController",   // 订单列表 segmentIndex
        "order/detail"          :"OrderDetailViewController",     // 订单详情 orderId
        "goods/detail"          :"GoodsDetailViewController",     // 商品详情productId
        "goods/list"            :"GoodsCategoryViewController", // type brandId 跳转到某个分类;跳转到某个品牌
        "goods/search"          :"SearchListViewController", // 搜索商品 text
        "coupon/list"           :"CouponListViewController",      // 优惠券列表
        "cart/list"             :"CartViewController",        // 购物车列表
        "address/list"          :"AddressListViewController",     // 收货地址列表
    ]


// 返回首页,然后指定选中模块
public static func backToTabBarController(index: NSInteger, completion:(()->())?=nil) {

guard let vc = EPCtrlManager.getTopVC(), let nav = vc.navigationController, let tabBarCtrl = nav.tabBarController  else {
return
}

nav.popToRootViewController(animated: false)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()+0.1) {
tabBarCtrl.selectedIndex = index
completion?()
}
}


// 销毁n个界面 不建议使用这个方法 可以在pushAppURLPath方法中设置destroyTime达到一样的效果,又可以避免用户侧滑返回
public static func popViewController(animated: Bool, time:NSInteger=1) {

guard let nav = EPCtrlManager.getTopVC()?.navigationController else {
return
}
let vcs = nav.viewControllers
let count = vcs.count
let index = (count - 1) - time
if index >= 0 {
let vc = vcs[index]
nav.popToViewController(vc, animated: true)
} else {
nav.popViewController(animated: true)
}
}


    /// 回到目标控制器
    public static func popViewController(targetVC: UIViewController.Type, animated: Bool, toRootVC: Bool=true) {

        popViewController(targetVCs: [targetVC], animated: animated, toRootVC: toRootVC)
    }

    

    /// 回到目标控制器[vc],从前到后 没有目标控制器是否回到根视图
    public static func popViewController(targetVCs: [UIViewController.Type], animated: Bool, toRootVC: Bool=true) {

        guard let nav = EPCtrlManager.getTopVC()?.navigationController else {
            return
        }
        let vcs = nav.viewControllers
        var canPop = false
        for vc in vcs {
            for tvc in targetVCs {
                if vc.isMember(of: tvc) {
                    canPop = true
                    nav.popToViewController(vc, animated: animated)
                    break
                }
            }
        }
        if !canPop && toRootVC {
            nav.popToRootViewController(animated: animated)
        }
    }

    /// push 一个vc --- destroyTime: push之前要销毁的几个压栈vc
    @objc public static func pushAppURLPath(_ path: String, query: [AnyHashable: Any]=[:], animated: Bool=true, destroyTime:NSInteger=0) {

        var urlString = "applicationScheme://"+path
        if path.contains("http://") || path.contains("https://") {
            urlString = path
        }
        pushURLString(urlString, query: query, animated: animated, destroyTime: destroyTime)
    }


    @objc public static func pushURLString(_ urlString: String, query: [AnyHashable: Any]=[:], animated: Bool=true, destroyTime:NSInteger=0) {

        guard let tvc = loadViewControllerWitURI(urlString, query: query) else {
            return
        }
        pushViewController(tvc, animated: animated, destroyTime: destroyTime)
    }


    @objc public static func pushViewController(_ tvc: UIViewController, query: [AnyHashable: Any]=[:], animated: Bool=true, destroyTime:NSInteger=0) {

        guard let vc = EPCtrlManager.getTopVC() else {
            return
        }

        if let _ = tvc.pushInfo {
            tvc.pushInfo?.merge(query, uniquingKeysWith: { (_, new) in new })
        }else {
            tvc.pushInfo = query
        }
        guard let nav = vc.navigationController else {
            vc.present(tvc, animated: true, completion: nil)
            return
        }
        tvc.hidesBottomBarWhenPushed = true

        if destroyTime > 0 {
            let vcs = nav.viewControllers
            let count = vcs.count
            var index = (count - 1) - destroyTime
            if index < 0 { // destroyTime 很多时,直接从根视图push
                index = 0
            }

            var reVCS = [UIViewController]()
            for vc in nav.viewControllers[0...index] {
                reVCS.append(vc)
            }
            reVCS.append(tvc)
            nav.setViewControllers(reVCS, animated: animated)
        }else {
            nav.pushViewController(tvc, animated: animated)
        }
    }

    public static func loadViewController(_ className: String, parameters: [AnyHashable: Any]? = nil) -> UIViewController? {

        var desVC: UIViewController?
        let spaceName = (Bundle.main.infoDictionary?["CFBundleExecutable"] as? String) ?? "ApplicationName"

        if let vc = storyboardClass(className) { // storyboard
            desVC = vc
        }else if let aClass = NSClassFromString("\(spaceName).\(className)") { // Swift
            if aClass is UIViewController.Type {
                let type = aClass as! UIViewController.Type
                desVC = type.init()
            }
        }else if let aClass = NSClassFromString("\(className)") { // OC
            if aClass is UIViewController.Type {
                let type = aClass as! UIViewController.Type
                desVC = type.init()
            }
        }

        desVC?.pushInfo = parameters
        return desVC
    }


    public static func loadViewController(_ viewController: UIViewController, parameters: [AnyHashable: Any]? = nil) -> UIViewController {

        viewController.pushInfo = parameters
        return viewController

    }

    public static func loadViewControllerWitURI(_ urlString: String, query: [AnyHashable: Any]? = nil) -> UIViewController? {

        

        // 先进行编码,防止有中文的带入, 不行进行二次编码
        var urlString = urlString
        if (URLComponents(string: urlString) == nil) {
            urlString = urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? urlString
        }

        guard let url = URLComponents(string: urlString), let scheme = url.scheme else {
            HGLog("无效的地址:\(urlString)")
            return nil
        }

        if scheme == "http" || scheme == "https" {

            let webVC = OtherWKWebViewController()
            webVC._urlStr = urlString
            return webVC

        } else if String(format: "%@://", scheme) == "appcationScheme://" {
            let path = (url.host ?? "") + url.path
            guard  var vcClassName = RouteDict[path] else {
                HGLog("没有配置视图控制器呢。。。:\(urlString)")
                return nil
            }

            var info: [AnyHashable: Any]?
            if query?.count ?? 0 > 0 {
                info = [AnyHashable: Any]()
                for (key, value) in query! {
                    info![key] = value
                }
            }

            if let queryItems = url.queryItems {
                if info == nil {
                    info = [AnyHashable: Any]()
                }
                for item in queryItems {
                    if let value = item.value {
                        info![item.name] = value
                    }
                }
            }
            return loadViewController(vcClassName, parameters: info)
        }

        HGLog("未知scheme:\(urlString)")
        return nil

    }

    

    private static func storyboardClass(_ className: String) -> UIViewController? {

        if className == "VIPWithdrawViewController" { // 提现
            let vc = UIStoryboard.init(name: "VIP", bundle: nil).instantiateViewController(withIdentifier: "withdrawTVC")
            return vc
        }else if className == "VIPRecordListViewController" { // 提现记录
            let vc = UIStoryboard.init(name: "VIP", bundle: nil).instantiateViewController(withIdentifier: "recordListVC")
            return vc
        }
        return nil
    }
}

用来跳转传递数据的扩展属性
extension UIViewController {

    private struct PushAssociatedKeys {
        static var pushInfo = "pushInfo"
    }

    @objc open var pushInfo: [AnyHashable: Any]? {
        get {
            return objc_getAssociatedObject(self, &PushAssociatedKeys.pushInfo) as? [AnyHashable : Any]
        }
        set(newValue) {
            objc_setAssociatedObject(self, &PushAssociatedKeys.pushInfo, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
}

可见视图控制器的获取
class EPCtrlManager: NSObject {

    public static let `default`: EPCtrlManager = {
        return EPCtrlManager()
    }()

    // MARK: **- 查找顶层控制器、**
    // 获取顶层控制器 根据window
    @objc public static func  getTopVC() -> UIViewController? {

        var window = UIApplication.shared.keyWindow
        //是否为当前显示的window
        if window?.windowLevel != UIWindow.Level.normal{
            let windows = UIApplication.shared.windows
            for  windowTemp in windows{
                if windowTemp.windowLevel == UIWindow.Level.normal{
                    window = windowTemp
                    break
                }
            }
        }
        let vc = window?.rootViewController
        return getTopVC(withCurrentVC: vc)
    }

    ///根据控制器获取 顶层控制器
    private static func  getTopVC(withCurrentVC VC :UIViewController?) -> UIViewController? {

        if VC == nil {
            print("🌶: 找不到顶层控制器")
            return nil
        }

        if let presentVC = VC?.presentedViewController {
            //modal出来的 控制器
            return getTopVC(withCurrentVC: presentVC)
        }else if let tabVC = VC as? UITabBarController {
            // tabBar 的跟控制器
            if let selectVC = tabVC.selectedViewController {
                return getTopVC(withCurrentVC: selectVC)
            }
            return nil
        } else if let naiVC = VC as? UINavigationController {
            // 控制器是 nav
            return getTopVC(withCurrentVC:naiVC.visibleViewController)
        } else {
            // 返回顶控制器
            return VC
        }
    }
}
收起阅读 »

Xcode 的拼写检查,你开启了吗?

iOS
Xcode 的拼写检查,你开启了吗?这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战引言作为一名开发人员,当我们编写代码时,我们会更多地关注逻辑和算法,而不是拼写和语法。但它也是我们编码的一个重要部分,特别是当我们从注释生成文档的时候。...
继续阅读 »

Xcode 的拼写检查,你开启了吗?

这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战


引言

作为一名开发人员,当我们编写代码时,我们会更多地关注逻辑和算法,而不是拼写和语法。但它也是我们编码的一个重要部分,特别是当我们从注释生成文档的时候。

拼写检查帮助我们找出拼写错误,让我们有更多的时间关注代码逻辑。


拼写检查能识别什么

答案就是代码中与Spelling and Grammer相关的所有内容

  • 变量名
  • 方法
  • 注释
  • 字符串的字面量(包括本地化)

先来看一段代码:

image.png

在上面的代码中,包括类、方法、变量和注释,但没有启用Spelling and Grammer。猛一看去,好像没啥问题,但如果我们仔细检查,就会发现很多拼写错误。

现在让我们启用Spelling and Grammer,看看会发生什么-

image.png

在上面的代码中我们可以看到,当我们启用拼写检查时,它能检测到所有的拼写错误,并用红色高亮显示。现在我们就省去了找错误的时间,可以直接去修改了。


如何开启

image.png

Edit > Format > Spelling and Grammar

可以看到有三个可用的选项,我们依次来看下:

Check Spelling While Typing

启用后,会把项目中的所有输入错误一次性、全部以红色高亮显示,就像上面的例子一样。

另外,开启这个选项后,还可以选中要修改的单词,然后右键,菜单中会出现 Xcode 建议的单词。

image.png

Check Document Now

它将在当前文件中逐个显示输入错误。为了检查当前文件中的所有错误,可以重复这个命令
Edit > Format > Spelling and Grammar > Check Document Now

或者使用快捷键
command 和分号(;)的组合

Show Spelling and Grammar

它会打开所有建议的更改。我们可以单击其中任何一个进行替换。 使用命令
Edit > Format > Spelling and Grammar > Show Spelling and Grammar

或者使用快捷键
command 和冒号(:)的组合

image.png


Learn Spelling 和 Ignore Spelling

有时候我们需要使用一些在系统词典中没有定义的独特词汇,比如应用程序前缀、开发者名称、公司名称等。Xcode 也会检查这些单词的错误。

所以就用Learn Spelling或者Ignore Spelling处理这些特殊的单词。

通过菜单

右键选中要处理的单词

image.png

通过 command + :

image.png


结语

快去探索一下 Edit > Format > Spelling and Grammar 下面的三个选项吧~

收起阅读 »

让你的 Swift 代码更 Swift

iOS
让你的 Swift 代码更 Swift这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战引言Swift 有很多其他语言所没有的独特的结构和方法,因此很多刚开始接触 Swift 的开发者并没有发挥它本身的优势。所以,我们就来看一看那些让你的...
继续阅读 »

让你的 Swift 代码更 Swift

这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战


引言

Swift 有很多其他语言所没有的独特的结构和方法,因此很多刚开始接触 Swift 的开发者并没有发挥它本身的优势。

所以,我们就来看一看那些让你的 Swift 代码更 Swift 的写法吧~


有条件的 for 循环

现在,我们要对view.subviews中的UIButton做一些不可描述的事情,用 for 循环怎么来遍历呢?

在下面的写法中,更推荐后面两种写法:


for subView in view.subviews {
if let button = subView as? UIButton {
//不可描述的事情
}
}


for case let button as UIButton in view.subviews {
//不可描述的事情
}


for button in view.subviews where button is UIButton {
//不可描述的事情
}



enumerated()

在 Swift 中进行 for 循环,要拿到下标值,一般的写法要么定义局部变量记录下标值,要么遍历 0..<view.subviews.count。其实还有个更方便的写法:enumerated(),可以一次性拿到下标值和遍历的元素。

  • ❌ 第一种肯定是不推荐的,因为还要定义额外的局部变量,容易出错,pass

  • ✅ 第二种在只需要用到下标值的时候,是可以用的,但如果还要用到下标值对应的元素,就还得再取一次,麻烦,pass

  • ✅ 第三种就比较完美,虽然一次性可以拿到下标值和元素,但其中一个用不到就可以用 _


var index: Int = 0
for subView in view.subviews {
//不可描述的事情
index += 1
}


for index in 0..<view.subviews.count {
let subView = view.subviews[index]
//不可描述的事情
}


//index 和 subView 在循环体中都能使用到
for (index, subView) in view.subviews.enumerated() {
//不可描述的事情
}

//只用到 index
for (index, _) in view.subviews.enumerated() {
//不可描述的事情
}

//只用到 subView
for (_, subView) in view.subviews.enumerated() {
//不可描述的事情
}


first(where: )

filter 是 Swift 中几个高级函数之一,过滤集合中的元素时非常的好用,不过在某些情况下,比如获取集合中满足条件的第一个元素时,有一个更好的选择first(where: )

let article1 = ArticleModel(title: "11", content: "内容1", articleID: "11111", comments: [])

let article2 = ArticleModel(title: "11", content: "内容2", articleID: "22222", comments: [])

let article3 = ArticleModel(title: "33", content: "内容3", articleID: "3333", comments: [])

let articles = [article1, article2, article3]


if let article = articles.filter({ $0.articleID == "11111" }).first {
print("\(article.title)-\(article.content)-\(article.articleID)")
}


if let article = articles.first(where: {$0.articleID == "11111"}) {
print("\(article.title)-\(article.content)-\(article.articleID)") //11-内容1-11111
}


contains(where: )

这个和上面的first(where: )几乎一样,比如这里要判断文章列表里是否包含 articleID 为 11111 的文章:


if !articles.filter({ $0.articleID == "11111" }).isEmpty {
//不可描述的事情
}


if articles.contains(where: { $0.articleID == "11111"}) {
//不可描述的事情
}


forEach

当循环体内的逻辑比较简单时,forEach 往往比 for...in...来的更加简洁:

func removeArticleBy(ID: String) {
//删库跑路
}


for article in articles {
removeArticleBy(ID: $0.articleID)
}


articles.forEach { removeArticleBy(ID: $0.articleID) }


计算属性 vs 方法

我们知道计算属性本身不存储数据,而是在 get 中返回计算后的值,在 set 中设置其他属性的值,所以和方法很类似,但比方法更简洁。一起来看下面的示例:


class YourManager {
static func shared() -> YourManager {
//不可描述的事情
}
}

let manager = YourManager.shared()


extension Date {
func formattedString() -> String {
//不可描述的事情
}
}

let string = Date().formattedString()



class YourManager {
static var shared: YourManager {
//不可描述的事情
}
}

let manager = YourManager.shared


extension Date {
var formattedString: String {
//不可描述的事情
}
}

let string = Date().formattedString


协议 vs 子类化

尽量使用协议而不是继承。协议可以让代码更加灵活,因为类可同时遵守多个协议。

此外,结构和枚举不能子类化,但是它们可以遵守协议,这就更加放大了协议的好处

Struct vs Class

尽可能使用 Struct 而不是 Class。Struct 在多线程环境中更安全,更快。

它们最主要的区别, Struct 是值类型,而 Classe 是引用类型,这意味着 Struct 的每个实例都有它自己的唯一副本,而 Class 的每个实例都有对数据的单个副本的引用。

这个链接是苹果官方的文档,解释如何在 Struct 和 Class 之间做出选择。 developer.apple.com/documentati…


结语

让我们的 Swift 代码更 Swift 的方法远不止上面这些,这里要说的是,平时写代码时,要刻意的使用 Swift 强大的特性,才能发挥它本身的价值。

而这些特性就需要大家去多看看官网的例子,或者一些主流的 Swift 第三方库,看看他们是如何运用 Swift 的特性的。

收起阅读 »

2022 年移动开发的最佳 React Native 替代方案

iOS
截至 2021 年 8 月,Android 和 iOS 平台占据移动操作系统市场份额的 99.15%。这些平台多年来一直主导着移动应用市场。结果是各种移动开发技术的兴起,包括跨平台框架。   React Native 是其中最受欢迎的一种...
继续阅读 »

截至 2021 年 8 月,Android 和 iOS 平台占据移动操作系统市场份额的 99.15%。这些平台多年来一直主导着移动应用市场。结果是各种移动开发技术的兴起,包括跨平台框架。  


image.png


React Native 是其中最受欢迎的一种。 


为什么?


React Native 允许开发人员跨平台共享多达 70% 的代码库。更快的开发、降低的成本和易于调试是该框架的一些好处。Facebook 的支持还确保 React Native 保持最佳运行状态。但是,就像其他所有框架一样,它也有其局限性。  


React Native 工程师经常面临兼容性问题和缺乏自定义模块。此外,使用此框架构建的应用程序因其近乎原生的功能而受到的性能影响较小。考虑到这一点,React Native 是一个不错的选择吗?这个问题的答案取决于您的产品要求。为了帮助您做出决定,我们编制了一份 React Native 替代方案列表,这些替代方案可为您的应用程序提供强大、便捷的功能。最后,您将能够知道要使用哪种技术。 


让我们开始吧!


需要考虑的 React Native 替代方案


原生平台:


本机应用程序编程语言是一些最流行的替代方案。它们是用于为操作系统开发移动应用程序的特定于平台的技术。此类操作系统的示例包括 Android、iOS 或 Windows。使用这些语言构建的本机应用程序往往会提供更好的性能和用户体验。开发人员对 Apple 应用程序使用 Swift 和 Objective-C,对原生 Android 应用程序使用 Java 和 Kotlin。


优点:




  • 出色的性能



这些编程语言直接与平台的底层资源交互。有了这个,开发人员可以充分利用系统的图形元素、计算功能或其他组件来构建快速执行的应用程序。 




  • 易于扩展  



在扩展应用程序的功能时,总会有遇到乏味问题的风险。本机代码减少了出现此问题的可能性。它们受 iOS 和 Android IDE 以及 SDK 工具包的支持。利用这一优势,您可以为每个平台实施基本、高级甚至最新的功能,而无需担心兼容性问题。  




  • 更容易使用



根据2021 年 Stack Overflow 开发人员调查,Swift 在其他 38 种编程语言中排名第 8。在类似的列表中,React Native 是 13 个框架中的第 9 个选择。Java 在最常用的语言中排名第 5。React Native 在 13 个最常用的框架中排名第 6。这表明这两个原生代码更易于使用和学习。使用它们来构建应用程序可以减轻中级和有经验的开发人员可能遇到的复杂性。


缺点




  • 开发成本高



Native 主要基于“一个产品,两个应用程序”的概念。因此,它可能会很昂贵,因为您需要两个对 iOS 和 Android 本机代码具有广泛知识的专业开发团队。




  • 耗时



Android 和 iOS 应用程序需要不同的代码库,这使得跨平台重用代码变得不可能。相反,每个产品都需要单独构建、测试、更新和管理。对于时间敏感的项目,这种缓慢的开发和部署过程是一个主要缺点。 




  • 稀缺人才库



尽管 Java 甚至在本机应用程序开发之外也被广泛采用,但该类别中的其他语言则相反。Stack Overflow 发现,Swift 和 Kotlin 分别被 5.1% 和 8.32% 的开发人员使用。或许,这可能归功于这些编程语言的年轻化。Objective-C 以 2.8% 位居榜首。但 React Native 遥遥领先,为 14.51%。因此,找到Swift 开发人员或其他对 Kotlin 和 Objective-C 具有广泛知识的编码人员可能会令人望而生畏。 


想阅读 React Native 和 Swift 之间的详细比较吗?阅读这篇文章


可以使用 Native Tech Stack 构建哪些应用程序/产品?


本机技术非常适合游戏应用程序、特定于操作系统的媒体播放器或其他需要完全访问设备功能的应用程序。


Flutter


image.png


Flutter 是 Google 于 2018 年创建并正式推出的一项年轻的开源技术。与 React Native 类似,Flutter 支持使用一个代码库来构建跨平台的类原生应用程序。它是用 Dart 开发的,Dart 是一种同样由 Google 提供的面向对象语言。多年来,Flutter 的受欢迎程度稳步上升,超过了其主要竞争对手 React Native。


优点




  • 更快的开发



与 React Native 一样,Flutter 允许更快的开发和部署时间。您可以从一个代码构建两个应用程序(iOS 和 Android)。它的小部件和交互式资产(例如,热重载)减轻了诸如测试和调试之类的繁琐任务。此外,Dart 是 Flutter 的编程语言。它快速、简洁,并且无需额外的抽象即可编译为本机代码。这总结了通过更短的上市时间实现快速开发和竞争优势。 




  • 优质的跨平台体验



Flutter 的 Material 和 Cupertino 小部件与 Apple 和 Google 的设计指南兼容。开发人员可以利用这些现成的 UI 元素在两个平台上构建具有令人印象深刻的界面的应用程序。更重要的是,Flutter 的渲染引擎 Skia 允许对每个像素进行完整的管理。这反过来又确保了使用 Flutter 构建的 UI 在多个平台或操作系统版本上启动时保持一致。




  • 轻松调试



使用热重载,无需重新启动整个应用程序即可查看更改。相反,Flutter 开发人员可以进行和查看实时更改,而无需在此之后重新编译代码。只需为两个平台构建一个应用程序这一事实确保检测到和修复的任何错误都将反映在两个版本中。




  • 低成本



就像使用 React Native 一样,使用 Flutter 开发应用程序的成本低于使用原生应用程序。这是因为您可以使用小型开发团队在更短的时间内为 iOS 和 Android 构建一个应用程序。  


缺点




  • 重量级应用



使用 Flutter 构建的应用程序文件很大。这些应用程序可能加载缓慢并占用空间和电池性能。为了扩大规模,开发人员可能经常使用较少的包和库,从而在某些功能上妥协。结果是质量低劣的产品。 




  • 技术不成熟



作为一个年轻的框架,Flutter 还没有广泛的资源基础。这意味着您可能找不到开发所需的第三方库和包。Flutter 不成熟的另一个缺点是它的增长潜力。未来不太有利的变化可能会给框架带来一些复杂性,使其更难管理。鉴于谷歌终止项目的历史,Flutter 也有可能不会持续下去。  




  • 对 iOS 功能的支持不佳



Flutter 允许快速、无缝地开发 Android 应用程序。但 iOS 的情况并非如此。访问平台的本机组件可能会出现问题。这使得几乎不可能实现特殊的 iOS 功能,例如引导访问或默认页面转换等简单功能。 


想阅读 React Native 和 Flutter 的详细比较吗?阅读这篇文章


Flutter 可以构建哪些应用/产品?


您可以使用 Flutter 开发需要快速或实时访问的产品。它包括客户服务、金融服务提供商、电子商务公司或任何接受当面付款的商家的应用程序。


Xamarin


image.png


另一种常见的 React Native 替代方案是 Xamarin。它是微软提供的跨平台技术。它始于 2011 年的 MonoTouch 和 Mono for Android,直到微软于 2016 年收购它。 Xamarin 使用 C# 语言和 .NET 框架来开发 iOS、Android 和 Windows 移动应用程序。 


优点




  • 快速发展



借助 Xamarin 的一种产品、一种技术堆栈方法,开发人员可以跨平台重用多达 90% 的代码。您无需在开发环境之间切换,因为您可以在 Visual Studio 中构建 Xamarin 应用程序。更重要的是,该框架允许访问所有支持平台上的公共资源。总而言之,开发时间更短,成本更低。 




  • 灵活的



Xamarin 的组件存储使开发人员可以访问跨平台的标准化 UI 控件、集成的开源库和第三方服务。借助这些广泛的资源,您可以选择多个元素或在您的应用中实现所需的功能。 




  • 出色的性能



Xamarin.Essentials 库提供对本机组件的访问。程序员可以使用 Xamarin.iOS 和 Xamarin.Android 分别构建 iOS 和 Android 应用程序。这些导致产品在性能上接近本机应用程序。React Native 在这方面并不接近。您还可以在运行时将应用程序的 UI 转换为原生元素,以确保接近原生的设计和性能。




  • 易于扩展



调试和维护更容易,因为开发人员可以从一个源代码跨平台发现和更改。此外,Xamarin 与其支持平台的 SDK 和 API 集成。一旦更改可用,这使得在 iOS 和 Android 应用程序中更新或实施新功能变得容易。  




  • 广泛的技术支持



Microsoft 提供学习资源和综合解决方案,使开发人员能够测试、监控和保护他们的应用程序。它包括 Azure 云、Xamarin Insights 和 Xamarin TestCloud。


缺点




  • 不适合图形繁重的应用程序 



在 Xamarin 中,开发人员主要可以共享业务逻辑而不是 UI 代码。这只是意味着您需要为每个平台构建一个单独的 UI。考虑到这一点,构建需要复杂动画或大量交互 UI 的游戏应用程序或其他产品会更慢且乏味。 




  • 有限的社区 



在最近的 Stack Overflow 开发人员调查中,只有 5.8% 的受访者使用 Xamarin。因此,可能很难聘请具有丰富经验和知识的Xamarin 开发人员。但是,随着框架的不断发展,这种劣势可能不会持续很长时间。如果您有紧急需求,请联系我们,让您与经过预先审查的 Xamarin 专家联系。 




  • 昂贵的许可证



Xamarin 加快了开发时间,降低了成本。但是,考虑到其 IDE(Microsoft Visual Studio)的价格,这种优势可能不那么令人印象深刻。对于商业项目,Enterprise 和 Professional 许可证是理想的选择。Enterprise 第一年的年度定价为每位用户 5,999 美元,然后续订 2,569 美元。首次专业订阅者将在以后支付 1,999 美元和 799 美元。 




  • 固有限制



尽管 Xamarin 是为原生应用开发量身定制的,但它并不是纯粹的原生应用。因此,它有几个限制。这包括对开源库的限制访问、更新或集成特定于平台的新 API 的延迟以及更大的应用程序大小。 


可以使用 Xamarin 构建哪些应用程序/产品?


Xamarin 在具有繁重逻辑或简单 UI 的应用程序上表现良好。它包括用于调查、项目管理、旅行、杂货或跟踪的应用程序。 


NativeScript


image.png


与 React Native 类似,该框架使用 JavaScript 为 iOS 和 Android 构建跨平台移动应用程序。它还支持 TypeScript、Angular 和相关框架。使用 NativeScript 构建的应用程序会生成完全原生的应用程序。 


优点




  • 原生功能



NativeScript 将 iOS 和 Android API 注入到 JS 虚拟机中,以便更容易地与原生资源集成。这使开发人员可以快速访问插件、Android SDK、iOS 依赖项管理器——Cocoapods 和其他相关技术,以构建具有本机性能的应用程序。它还带来了直观的用户界面和更好的用户体验。




  • 更广泛的开发人才



NativeScript 使用 JS 和 CSS 的一个子集,它们都是成熟的。对这些技术有一定了解的开发人员可以更快地构建本机应用程序。此外,这个 NativeScript 支持各种 JS 框架,例如 Angular、Vue.js 或 TypeScript。 




  • 更少的开发时间



使用 NativeScript 构建时,开发人员可以在模拟器屏幕上实时查看代码更改。因此,此后您无需重新编译应用程序。再加上 NativeScript 中的单一代码库方法,这意味着每次修改都可以应用于其他平台。因此,该框架提高了开发速度。 


缺点




  • 本土专业知识



根据您的项目范围,您可能需要实现高级本机功能。这需要在特定于平台的 UI 标记和元素方面具有专业知识的软件顾问




  • 插件质量不确定



虽然 NativeScript 上有几个免费插件,但并不是全部都经过验证。这使开发人员面临使用有问题的开源插件的风险,这些插件可能会导致严重的瓶颈或更糟糕的最终产品。




  • 比本机更大的应用程序大小



无论 NativeScript 应用程序与真正的 Native 多么接近,它们的大小都相对较大。NativeScript 上空白 android 项目的默认大小为 12MB。但这仍然低于 React Native 的默认 APK 大小,它可以高达 23MB 


可以使用 NativeScript 构建哪些应用程序/产品?


NativeScript 最适合需要利用硬件组件功能的实时应用程序或产品。它包括用于流媒体、实时馈送和简单游戏的应用程序。 


Ionic


image.png


Ionic 是一种 React Native 替代方案,可让您构建跨平台应用程序。这个开源 SDK 最初是基于 Apache Cordova 和 AngularJS 构建的。但后来,它增加了对 React、Vue.js 和 Angular 等其他 JS 框架的支持。 


优点




  • 原生功能




使用 Apache Cordova 和 Capacitor 插件,Ionic 可以访问移动操作系统的相机、蓝牙、麦克风、指纹扫描仪、GPS 等功能。此外,Ionic 的 UI 组件及其内置的自适应样式通过对设计进行轻微更改来确保应用程序保持原生的感觉。 




  • 跨平台体验



Ionic 利用网络标准和通用 API 为任何平台构建应用程序。有了这个,开发人员可以构建一个应用程序,然后使用一个代码库将它定制到所有支持的平台。 




  • 更短的开发时间



使用 Ionic 的预构建功能,无需为每个开发构建 UI 组件。相反,开发人员可以重用或自定义每个元素,在更短的时间内构建功能性应用程序。 


缺点




  • 不适合游戏应用



与大多数跨平台框架一样,Ionic 可能不适合具有高级图形的应用程序。这是因为 Ionic 使用 CSS,这在开发 3D 游戏应用程序时受到限制。在这种情况下,本地化可能是最好的选择。 




  • 兼容性问题



集成的本机插件可能会相互冲突,从而产生大大减慢开发过程的问题。 




  • 安全问题




开发跨平台意味着您需要同时考虑 Web 和本机移动应用程序的安全性。尽管现有解决方案可以解决此问题,但对于需要高端安全性的应用程序而言,这可能既乏味又昂贵。 


想要阅读 React Native 和 Ionic 之间的详细比较吗?阅读这篇文章


可以使用 Ionic 构建哪些应用程序/产品?


Ionic 可用于需要即时信息或类似本机功能的应用程序。这包括用于新闻、生活方式、流媒体和金融服务的应用程序。 


Apache Cordova


image.png


Apache Cordova 由 Nitobi 创建,于 2011 年被 Adobe 收购,并更名为 PhoneGap。随后,它作为 PhoneGap 的开源版本发布。Apache Cordova 使开发人员能够使用 HTML、CSS 和 JavaScript 构建移动应用程序。可以通过命令行界面 (CLI) 使用此 React Native 替代方案开发跨平台应用程序。对于接近本机的应用程序,您可以使用 Cordova 以平台为中心的工作流程。 


优点




  • 丰富的插件集



开发人员在使用 Apache Cordova 进行构建时有大量插件可供选择。这些插件提供对本机设备 API 的访问,从而更轻松地在应用程序中实现广泛的功能,以获得更好的性能和用户体验。 




  • 无障碍技能集



Cordova 使用的标准技术 JS、CSS 和 HTML 已经成熟。具有这些技术编程背景的移动开发人员可以快速适应构建 Apache Cordova 应用程序。易于找到开发人员、温和的学习曲线和快速的上市时间潜力是直接的好处。




  • 跨平台支持



本着“一次编写,随处运行”的原则,代码可以跨平台重用。这确保了应用程序可以适应任何平台的UI。此外,无需将特定于平台的编程语言作为一个代码库来学习可以胜任。


缺点




  • 特定于平台的限制



因为 Apache Cordova 应用程序不是纯原生的,它们依赖插件来利用设备的功能。这些第三方自定义插件可能不容易获得、更新或跨平台兼容。 




  • 可能需要本地开发人员



如前所述,使用 Cordova 构建的应用程序可能会遇到某些插件的兼容性问题。您可能需要可以从头开始编写自定义插件的专业本机开发人员。这转化为延长的开发时间和成本。 




  • 潜在的性能问题



使用 Cordova 的默认功能构建高性能应用程序可能很困难。这是因为其技术中存在的限制会减慢应用程序的速度。此类缺点在于其 WebView 和移动浏览器组件以及 JavaScript 中缺乏多线程功能。


可以使用 Apache Cordova 构建哪些应用程序/产品?


您可以使用 Cordova 开发结合本机组件和 WebView 以访问设备 API 的应用程序。它包括用于健身、运动、跟踪和市场的应用程序。 


Framework 7


image.png


Framework 7 是您应该考虑的另一个 React Native 替代方案。它是一个开源 HTML 框架,用于构建具有近乎本机功能的混合 Web 和移动应用程序。Framework 7 兼容 Android 和 iOS 平台。


优点




  • 反应灵敏



从基本元素到高级元素,Framework 7 具有广泛的 UI 组件。开发人员可以访问诸如延迟加载、无限滚动、复选框列表等控件。使用这些资源构建具有干净、本机界面的动态应用程序。




  • 多框架支持



Framework 7 可以与 Angular、React 和 Vue.js 等 JS 框架一起使用。这些结构为开发过程贡献了它们的力量和简单性




  • 对开发者友好



开发人员不仅限于自定义标签。在使用 Framework 7 时,他们可以轻松地使用由 JS 和 CSS 补充的纯 HTML 代码。这意味着至少具有这些语言甚至 jQuery 中级知识的程序员可以扩展。 


缺点




  • 有限的平台支持



目前,Framework 7 仅支持 iOS 和 Android 平台。希望为其他平台开发应用程序的开发人员可能会评估其他框架。




  • iOS 专用



Framework 7 最初是为 Apple 环境开发的。这开辟了在为 Android 开发时遇到渲染问题的可能性。




  • 最少的文档



用户可以轻松找到有关如何在此框架中实现任何元素集的资源。然而,大多数高级需求可能没有现成的答案,因为文档不像其他框架那样广泛。  


Framework 7 可以构建哪些应用程序/产品?


Framework 7 可用于构建依赖于设备硬件的渐进式 Web 应用程序或 iOS 和 Android 应用程序。 


jQuery Mobile


image.png


jQuery Mobile 是一个开源 JavaScript 库,用于开发跨平台移动应用程序和网站。它利用了 jQuery 的特性,jQuery 以实现动画、AJAX 和文档对象模型 (DOM) 操作的简便性和快速性而闻名。  


优点




  • 较低的学习曲线



这项技术建立在 jQuery Core 之上,大多数程序员可能已经在过去使用过它。这使得它更容易学习和使用。




  • 跨平台、跨浏览器兼容性



使用 jQuery Mobile 框架,您可以构建与流行的桌面浏览器和平台兼容的高度响应的应用程序和网站。其支持的平台包括 iOS、Android、Windows、WebOS 和 Blackberry。 




  • 出色的动画页面过渡效果



基于渐进式增强原理,jQuery Mobile 导航系统允许页面通过 Ajax 加载到 DOM。这确保了页面得到改进,然后以高质量的过渡显示。




  • 简单方便



开发人员只需几行代码即可处理 HTML 事件、AJAX 请求和 DOM 操作。这在 JavaScript 中需要更长的行。 




  • 轻量级



由于其有限的图像依赖性,jQuery Mobile 的最小大小为 40 KB。这有助于它的速度。 


缺点




  • 最小主题



jQuery 移动版中可用的 CSS 主题使自定义应用程序变得容易。然而,它们是有限的。开发人员可能会构建与使用此技术构建的其他产品不同的应用程序。




  • 使用其他框架非常耗时




jQuery Mobile 与 PhoneGap 等其他移动应用程序框架相结合,以获得更好的性能。但这会减慢开发过程。 




  • 移动设备运行速度较慢



即使在最新的 iOS 和 Android 平台上,这项技术也明显变慢。如果您希望开发一个快速的移动应用程序,您可能需要考虑其他替代方案。


可以使用 jQuery Mobile 构建哪些应用程序/产品?


jQuery Mobile 是针对旧浏览器、内容管理系统或其他需要一些动画和较少用户交互的产品的应用程序的理想选择。 


PhoneGap


image.png


渐进式 Web 应用程序 (PWA)


渐进式 Web 应用程序是应用程序软件,可以像常规网站一样在 Web 浏览器上加载和执行。它结合了 Web 功能和本机应用程序的功能(例如推送通知和对硬件功能的访问),以提供出色的用户体验。与传统应用程序不同,PWA 无法从应用程序商店安装到设备中。相反,它可以添加为用户的主屏幕。渐进式 Web 应用程序使用 HTML、JavaScript 和 CSS 等标准 Web 技术构建。 


优点:




  • 反应灵敏



PWA 可以轻松适应多种设备的屏幕尺寸,无论是平板电脑、台式机、Android 和 iOS 移动设备,还是其他直接尺寸。 




  • 安全的



利用 HTTPS,在 PWA 上广播的信息被加密。在大多数情况下,如果没有安全连接,用户将无法访问某些功能,例如地理定位。这提供了高端安全性和针对路径攻击或其他网络威胁的更多保护。 




  • 极具吸引力的用户体验



PWA 是使用渐进改进原则构建的。这些应用程序在符合标准的浏览器上提供更好的用户体验,在不符合标准的浏览器上至少提供可接受的界面。此外,这些应用程序通过现代网络标准提供本机应用程序功能和感觉。这些功能进一步丰富了移动体验。 




  • 减少对网络的依赖



构建渐进式 Web 应用程序的最大优势之一是它们能够在连接速度缓慢的情况下运行。如果用户访问过某个站点,即使没有网络,他们也可以访问该内容。这可以通过 Service Workers、缓存 API 和离线存储站点资产的客户端存储技术实现。也就是说,PWA 利用这一点来享受更快的加载速度。 




  • 易于访问和维护



作为一个基于网络的应用程序,PWA 享有更高的知名度,因为它可以被搜索引擎发现和排名,给他们更多的知名度。此外,用户无需额外安装即可轻松进行测试和升级,因为这些应用程序可以在线访问。 


缺点




  • 对硬件组件的访问受限



虽然它可以访问相当多的功能,但 PWA 无法完全使用设备的大量硬件组件。对高级相机控制、通话功能、蓝牙的支持,并且某些功能在某些设备中仍然不发达。




  • 弱 iOS 支持



iOS 设备中 PWA 的一个常见缺点是缺乏推送通知支持。这使得无法通过新内容或更新重新吸引 iOS 用户,从而导致转化次数减少。 




  • 没有可靠的第三方控制



因为 PWA 不能从应用商店下载,所以没有监管标准。因此,其大多数类本机应用程序的 UI 质量可能不一致。 


哪些应用程序/产品可以构建为渐进式 Web 应用程序?


PWA 最适用于由于网络缓慢而易于失败的软件、需要更高流量的应用程序或很少使用的应用程序。它包括为电子商务公司、叫车服务、市场代理等提供的产品。


Bootstrap


image.png


Bootstrap 是一个结合了 Javascript、CSS 和 HTML 的工具包。它广泛用于开发响应式、移动优先的网页和完全嵌入浏览器的渐进式 Web 应用程序 (PWA)。 


什么是 PWA?


渐进式 Web 应用程序是应用程序软件,可以像常规网站一样在 Web 浏览器上加载和执行。它结合了 Web 功能和本机应用程序的功能(例如推送通知和对硬件功能的访问),以提供出色的用户体验。与传统应用程序不同,PWA 无法从应用程序商店安装到设备中。相反,它可以添加为用户的主屏幕。 


优点




  • 高度响应



Bootstrap 的流体网格系统是其主要优势之一。它具有定义明确的类和各种简单的布局。一旦实施,它将在所有平台上提供一致的外观。这些组件也可以定制以匹配每个项目的设计。 




  • 广泛的文档



Bootstrap被称为“世界上最流行的 HTML、CSS 和 JS 库”,拥有丰富的文档。考虑到这一点,移动开发人员很可能会为此框架找到基本和高级问题的解决方案。 




  • 对jQuery插件的内置支持




通过这些内置插件,Bootstrap 可以从 JS API 访问更多 UI 组件。工具提示和对话框等界面也可以提高预先存在的界面的性能。 




  • 安稳



Bootstrap 的 PWA 通过 HTTPS 广播信息。在大多数情况下,如果没有安全连接,用户将无法访问某些功能,例如地理定位。这提供了针对大多数网络威胁的高端安全性和更多保护。


缺点




  • 设备功能有限



在默认模式下使用 Bootstrap 可以将几个未使用的元素和代码加载到您的项目中。这会转化为较大的应用程序大小和缓慢的加载时间。




  • 其他自定义设置




使用此框架构建需要智能手机广泛功能的 Web 应用程序并不是一个好的选择。原因是用 JS 和 Bootstrap 编写的 Web 应用程序无法完全访问设备的传感器和功能。




  • 可能对开发人员不友好



使用 Bootstrap 默认组件开发的 Web 应用看起来很相似。要自定义应用程序,您需要手动覆盖样式表。这个额外的步骤通常会破坏使用这个框架的目的。 




  • 可能对开发人员不友好



某些任务(例如访问预定义的类或自定义)可能需要更长的时间来学习。 


可以使用 Bootstrap 构建哪些应用程序/产品?


Bootstrap 主要用于设计响应式网页和网络应用程序。 


image.png


image.png


最后


在竞争激烈的移动应用程序开发世界中,错过跨多个平台构建应用程序是一个很大的风险。选择正确的替代方案可以帮助您在重要的平台上保持存在感,同时降低开发成本。


链接:https://juejin.cn/post/7036615302007750692
来源:稀土掘金
收起阅读 »

vscode调试入门——不要只会console了!什么是launch.json?

前言 记得我还是一个小菜鸡的时候,就有人问过我,都用什么调试,我红着脸说到,我只会用console调试。羞愧的我想再继续掌握一下vscode调试的方法,可惜当时没有找到很好的教程,加上相关基础较差,只能是一知半解。如今进化为大菜鸡的我,总结一下基础的vscod...
继续阅读 »

前言


记得我还是一个小菜鸡的时候,就有人问过我,都用什么调试,我红着脸说到,我只会用console调试。羞愧的我想再继续掌握一下vscode调试的方法,可惜当时没有找到很好的教程,加上相关基础较差,只能是一知半解。如今进化为大菜鸡的我,总结一下基础的vscode调试


基本调试


可以说vscode对js代码的调试非常友好了,它内置了node的debugger插件,如果是要用vscdoe调试python,c++等还是要后续安装插件的


基本的调试方法很简单,写一段简单的代码


图片.png


然后在调试项里找到这个小三角箭头


图片.png


然后就可以进入node的调试界面


图片.png


怎么样!是不是很简单就达到了我想要的效果,比单纯一个个console出来要更好


深入一下


上边的方法虽然很简单,但是只适用比较简单的情况,对于大多数调试场景,去创建launch配置文件是更好的,因为它允许配置和保存调试设置细节


launch.json


当你刚开始创建还没有launch.json的时候 vscoed会自动帮你自动检测你的debug环境,开始debug
但如果失败了,会让你进行选择


图片.png


然后他会在你当前工作区下,给你创建个.vscode文件夹,里面有我们要的launch.json文件。简单说我们可以通过这个文件可配置的debug


假如你的launch.json文件是这样的(搞懂意思就好)



{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": ["<node_internals>/**"],

}
]
}

其中


type属性是指你这次debug的类型 我这里介绍常用的两类 node和chrome 下边都会说到


request 指的是请求配置类型,氛围launch和attach


name 指的就是你这一条调试配置的name,会出现在start绿箭头,选择具体方式的时候


这几个是必用的


有更多的会在之后涉及,了解更多可以看这里的文档


attach还是launch


这是两个核心的debugging模式,用不同方式去处理工作流


简单来说 launch会在 你调试的工具 也就是我们用的vscode 启动另外的应用,这很适合你习惯于用浏览器的方法


attach 意为附加 会在你的开发者工具上附加调试程序


chrome debugger


除了用上述的type为node的调试方法,我们也可以用调用的chrome的工作台去调试


这里要安装一个插件 debugger for chrome 我在之前的文章当你买了新的mac 曾经提到过


当安装好之后


你就可以在我们的launch.json里添加配置啦!


图片.png


假如我们添加一个这样的配置


{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome",
"url": "http://localhost:8080",
"file": "${workspaceFolder}/index.html"
},

file 值得就是打开的文件 workspaceFolder是我们当前的工作区


假如我们的index.html是这样的并打上断点


图片.png


可以进入我们的chrome调试页面


图片.png


总结


基本的调试入门方法就是这样啦,其实还有更深层的内容,我会继续学习,完善这篇文章


作者:douxpang
链接:https://juejin.cn/post/6956832271236071431

收起阅读 »

前端架构师的 git 功力,你有几成火候?

Git
分支管理策略 git 分支强大的同时也非常灵活,如果没有一个好的分支管理策略,团队人员随意合并推送,就会造成分支混乱,各种覆盖,冲突,丢失等问题。 目前最流行的分支管理策略,也称工作流(Workflow),主要包含三种: Git Flow GitHub Fl...
继续阅读 »

分支管理策略


git 分支强大的同时也非常灵活,如果没有一个好的分支管理策略,团队人员随意合并推送,就会造成分支混乱,各种覆盖,冲突,丢失等问题。


目前最流行的分支管理策略,也称工作流(Workflow),主要包含三种:



  • Git Flow

  • GitHub Flow

  • GitLab Flow


我司前端团队结合实际情况,制定出自己的一套分支管理策略。


我们将分支分为 4 个大类:



  • dev-*

  • develop

  • staging

  • release


dev-* 是一组开发分支的统称,包括个人分支,模块分支,修复分支等,团队开发人员在这组分支上进行开发。


开发前,先通过 merge 合并 develop 分支的最新代码;开发完成后,必须通过 cherry-pick 合并回 develop 分支。


develop 是一个单独分支,对应开发环境,保留最新的完整的开发代码。它只接受 cherry-pick 的合并,不允许使用 merge。


staging 分支对应测试环境。当 develop 分支有更新并且准备发布测试时,staging 要通过 rebase 合并 develop 分支,然后将最新代码发布到测试服务器,供测试人员测试。


测试发现问题后,再走 dev-* -> develop -> staging 的流程,直到测试通过。


release 则表示生产环境。release 分支的最新提交永远与线上生产环境代码保持同步,也就是说,release 分支是随时可发布的。


当 staging 测试通过后,release 分支通过 rebase 合并 staging 分支,然后将最新代码发布到生产服务器。


总结下合并规则:



  • develop -> (merge) -> dev-*

  • dev-* -> (cherry-pick) -> develop

  • develop -> (rebase) -> staging

  • staging -> (rebase) -> release


为什么合并到 develop 必须用 cherry-pick?


使用 merge 合并,如果有冲突,会产生分叉;dev-* 分支多而杂,直接 merge 到 develop 会产生错综复杂的分叉,难以理清提交进度。


而 cherry-pick 只将需要的 commit 合并到 develop 分支上,且不会产生分叉,使 git 提交图谱(git graph)永远保持一条直线。


再有,模块开发分支完成后,需要将多个 commit 合为一个 commit,再合并到 develop 分支,避免了多余的 commit,这也是不用 merge 的原因之一。


为什么合并到 staging/release 必须用 rebase?


rebase 译为变基,合并同样不会产生分叉。当 develop 更新了许多功能,要合并到 staging 测试,不可能用 cherry-pick 一个一个把 commit 合并过去。因此要通过 rebase 一次性合并过去,并且保证了 staging 与 develop 完全同步。


release 也一样,测试通过后,用 rebase 一次性将 staging 合并过去,同样保证了 staging 与 release 完全同步。


commit 规范与提交验证


commit 规范是指 git commit 时填写的描述信息,要符合统一规范。


试想,如果团队成员的 commit 是随意填写的,在协作开发和 review 代码时,其他人根本不知道这个 commit 是完成了什么功能,或是修复了什么 Bug,很难把控进度。


为了直观的看出 commit 的更新内容,开发者社区诞生了一种规范,将 commit 按照功能划分,加一些固定前缀,比如 fix:feat:,用来标记这个 commit 主要做了什么事情。


目前主流的前缀包括以下部分:



  • build:表示构建,发布版本可用这个

  • ci:更新 CI/CD 等自动化配置

  • chore:杂项,其他更改

  • docs:更新文档

  • feat:常用,表示新增功能

  • fix:常用:表示修复 bug

  • perf:性能优化

  • refactor:重构

  • revert:代码回滚

  • style:样式更改

  • test:单元测试更改


这些前缀每次提交都要写,刚开始很多人还是记不住的。这里推荐一个非常好用的工具,可以自动生成前缀。地址在这里


首先全局安装:


npm install -g commitizen cz-conventional-changelog

创建 ~/.czrc 文件,写入如下内容:


{ "path": "cz-conventional-changelog" }

现在可以用 git cz 命令来代替 git commit 命令,效果如下:


WX20210922.png


然后上下箭选择前缀,根据提示即可方便的创建符合规范的提交。


有了规范之后,光靠人的自觉遵守是不行的,还要在流程上对提交信息进行校验。


这个时候,我们要用到一个新东西 —— git hook,也就是 git 钩子。


git hook 的作用是在 git 动作发生前后触发自定义脚本。这些动作包括提交,合并,推送等,我们可以利用这些钩子在 git 流程的各个环节实现自己的业务逻辑。


git hook 分为客户端 hook 和服务端 hook。


客户端 hook 主要有四个:



  • pre-commit:提交信息前运行,可检查暂存区的代码

  • prepare-commit-msg:不常用

  • commit-msg:非常重要,检查提交信息就用这个钩子

  • post-commit:提交完成后运行


服务端 hook 包括:



  • pre-receive:非常重要,推送前的各种检查都在这

  • post-receive:不常用

  • update:不常用


大多数团队是在客户端做校验,所以我们用 commit-msg 钩子在客户端对 commit 信息做校验。


幸运的是,不需要我们手动去写校验逻辑,社区有成熟的方案:husky + commitlint


husky 是创建 git 客户端钩子的神器,commitlint 是校验 commit 信息是否符合上述规范。两者配合,可以阻止创建不符合 commit 规范的提交,从源头保证提交的规范。


husky + commitlint 的具体使用方法请看这里


误操作的撤回方案


开发中频繁使用 git 拉取推送代码,难免会有误操作。这个时候不要慌,git 支持绝大多数场景的撤回方案,我们来总结一下。


撤回主要是两个命令:resetrevert


git reset


reset 命令的原理是根据 commitId 来恢复版本。因为每次提交都会生成一个 commitId,所以说 reset 可以帮你恢复到历史的任何一个版本。



这里的版本和提交是一个意思,一个 commitId 就是一个版本



reset 命令格式如下:


$ git reset [option] [commitId]

比如,要撤回到某一次提交,命令是这样:


$ git reset --hard cc7b5be

上面的命令,commitId 是如何获取的?很简单,用 git log 命令查看提交记录,可以看到 commitId 值,这个值很长,我们取前 7 位即可。


这里的 option 用的是 --hard,其实共有 3 个值,具体含义如下:



  • --hard:撤销 commit,撤销 add,删除工作区改动代码

  • --mixed:默认参数。撤销 commit,撤销 add,还原工作区改动代码

  • --soft:撤销 commit,不撤销 add,还原工作区改动代码


这里要格外注意 --hard,使用这个参数恢复会删除工作区代码。也就是说,如果你的项目中有未提交的代码,使用该参数会直接删除掉,不可恢复,慎重啊!


除了使用 commitId 恢复,git reset 还提供了恢复到上一次提交的快捷方式:


$ git reset --soft HEAD^

HEAD^ 表示上一个提交,可多次使用。


其实平日开发中最多的误操作是这样:刚刚提交完,突然发现了问题,比如提交信息没写好,或者代码更改有遗漏,这时需要撤回到上次提交,修改代码,然后重新提交。


这个流程大致是这样的:


# 1. 回退到上次提交
$ git reset HEAD^
# 2. 修改代码...
...
# 3. 加入暂存
$ git add .
# 4. 重新提交
$ git commit -m 'fix: ***'

针对这个流程,git 还提供了一个更便捷的方法:


$ git commit --amend

这个命令会直接修改当前的提交信息。如果代码有更改,先执行 git add,然后再执行这个命令,比上述的流程更快捷更方便。


reset 还有一个非常重要的特性,就是真正的后退一个版本


什么意思呢?比如说当前提交,你已经推送到了远程仓库;现在你用 reset 撤回了一次提交,此时本地 git 仓库要落后于远程仓库一个版本。此时你再 push,远程仓库会拒绝,要求你先 pull。


如果你需要远程仓库也后退版本,就需要 -f 参数,强制推送,这时本地代码会覆盖远程代码。


注意,-f 参数非常危险!如果你对 git 原理和命令行不是非常熟悉,切记不要用这个参数。


那撤回上一个版本的代码,怎么同步到远程更安全呢?


方案就是下面要说的第二个命令:git revert


git revert


revert 与 reset 的作用一样,都是恢复版本,但是它们两的实现方式不同。


简单来说,reset 直接恢复到上一个提交,工作区代码自然也是上一个提交的代码;而 revert 是新增一个提交,但是这个提交是使用上一个提交的代码。


因此,它们两恢复后的代码是一致的,区别是一个新增提交(revert),一个回退提交(reset)。


正因为 revert 永远是在新增提交,因此本地仓库版本永远不可能落后于远程仓库,可以直接推送到远程仓库,故而解决了 reset 后推送需要加 -f 参数的问题,提高了安全性。


说完了原理,我们再看一下使用方法:


$ git revert -n [commitId]

掌握了原理使用就很简单,只要一个 commitId 就可以了。


Tag 与生产环境


git 支持对于历史的某个提交,打一个 tag 标签,常用于标识重要的版本更新。


目前普遍的做法是,用 tag 来表示生产环境的版本。当最新的提交通过测试,准备发布之时,我们就可以创建一个 tag,表示要发布的生产环境版本。


比如我要发一个 v1.2.4 的版本:


$ git tag -a v1.2.4 -m "my version 1.2.4"

然后可以查看:


$ git show v1.2.4

> tag v1.2.4
Tagger: ruims <2218466341@qq.com>
Date: Sun Sep 26 10:24:30 2021 +0800

my version 1.2.4

最后用 git push 将 tag 推到远程:


$ git push origin v1.2.4

这里注意:tag 和在哪个分支创建是没有关系的,tag 只是提交的别名。因此 commit 的能力 tag 均可使用,比如上面说的 git resetgit revert 命令。


当生产环境出问题,需要版本回退时,可以这样:


$ git revert [pre-tag]
# 若上一个版本是 v1.2.3,则:
$ git revert v1.2.3

在频繁更新,commit 数量庞大的仓库里,用 tag 标识版本显然更清爽,可读性更佳。


再换一个角度思考 tag 的用处。


上面分支管理策略的部分说过,release 分支与生产环境代码同步。在 CI/CD(下面会讲到)持续部署的流程中,我们是监听 release 分支的推送然后触发自动构建。


那是不是也可以监听 tag 推送再触发自动构建,这样版本更新的直观性是不是更好?


诸多用处,还待大家思考。


永久杜绝 443 Timeout


我们团队内部的代码仓库是 GitHub,众所周知的原因,GitHub 拉取和推送的速度非常慢,甚至直接报错:443 Timeout。


我们开始的方案是,全员开启 VPN。虽然大多时候速度不错,但是确实有偶尔的一个小时,甚至一天,代码死活推不上去,严重影响开发进度。


后来突然想到,速度慢超时是因为被墙,比如 GitHub 首页打不开。再究其根源,被墙的是访问网站时的 http 或 https 协议,那么其他协议是不是就不会有墙的情况?


想到就做。我们发现 GitHub 除了默认的 https 协议,还支持 ssh 协议。于是准备尝试一下使用 ssh 协议克隆代码。


用 ssh 协议比较麻烦的一点,是要配置免密登录,否则每次 pull/push 时都要输入账号密码。


GitHub 配置 SSH 的官方文档在这里


英文吃力的同学,可以看这里


总之,生成公钥后,打开 GitHub 首页,点 Account -> Settings -> SSH and GPG keys -> Add SSH key,然后将公钥粘贴进去即可。


现在,我们用 ssh 协议克隆代码,例子如下:


$ git clone git@github.com:[organi-name]/[project-name]

发现瞬间克隆下来了!再测几次 pull/push,速度飞起!


不管你用哪个代码管理平台,如果遇到 443 Timeout 问题,请试试 ssh 协议!


hook 实现部署?


利用 git hook 实现部署,应该是 hook 的高级应用了。


现在有很多工具,比如 GitHub,GitLab,都提供了持续集成功能,也就是监听某一分支推送,然后触发自动构建,并自动部署。


其实,不管这些工具有多少花样,核心的功能(监听和构建)还是由 git 提供。只不过在核心功能上做了与自家平台更好的融合。


我们今天就抛开这些工具,追本溯源,使用纯 git 实现一个 react 项目的自动部署。掌握了这套核心逻辑,其他任何平台的持续部署也就没那么神秘了。


由于这一部分内容较多,所以单独拆出去一篇文章,地址如下:


纯 Git 实现前端 CI/CD


终极应用: CI/CD


上面的一些地方也提到了持续集成,持续部署这些字眼,现在,千呼万唤始出来,主角正式登场了!


可以这么说,上面写到的所有规范规则,都是为了更好的设计和实现这个主角 ——— CI/CD。


首先了解一下,什么是 CI/CD ?


核心概念,CI(Continuous Integration)译为持续集成,CD 包括两部分,持续交付(Continuous Delivery)和持续部署(Continuous Deployment)


从全局看,CI/CD 是一种通过自动化流程来频繁向客户交付应用的方法。这个流程贯穿了应用的集成,测试,交付和部署的整个生命周期,统称为 “CI/CD 管道”。


虽然都是像流水线一样自动化的管道,但是 CI 和 CD 各有分工。


持续集成是频繁地将代码集成到主干分支。当新代码提交,会自动执行构建、测试,测试通过则自动合并到主干分支,实现了产品快速迭代的同时保持高质量。


持续交付是频繁地将软件的新版本,交付给质量团队或者用户,以供评审。评审通过则可以发布生产环境。持续交付要求代码(某个分支的最新提交)是随时可发布的状态。


持续部署是代码通过评审后,自动部署到生产环境。持续部署要求代码(某个分支的最新提交)是随时可部署的。


持续部署与持续交付的唯一区别,就是部署到生产环境这一步,是否是自动化


部署自动化,看似是小小的一步,但是在实践过程中你会发现,这反而是 CI/CD 流水线中最难落实的一环。


为什么?首先,从持续集成到持续交付,这些个环节都是由开发团队实施的。我们通过团队内部协作,产出了新版本的待发布的应用。


然而将应用部署到服务器,这是运维团队的工作。我们要实现部署,就要与运维团队沟通,然而开发同学不了解服务器,运维同学不了解代码,沟通起来困难重重。


再有,运维是手动部署,我们要实现自动部署,就要有服务器权限,与服务器交互。这也是个大问题,因为运维团队一定会顾虑安全问题,因而推动起来节节受阻。


目前社区成熟的 CI/CD 方案有很多,比如老牌的 jenkins,react 使用的 circleci,还有我认为最好用的GitHub Action等,我们可以将这些方案接入到自己的系统当中。


这篇文章篇幅已经很长了,就到这里结束吧。接下来我会基于 GitHub Action 单独出一篇详细的 react 前端项目 CI/CD 实践,记得关注我的专栏哦。



作者:杨成功
链接:https://juejin.cn/post/7024043015794589727

收起阅读 »

先睹为快即将到来的HTML6

HTML,超文本标记语言,是一种用于创建网页的标准标记语言。自从引入 HTML 以来,它就一直用于构建互联网。与 JavaScript 和 CSS 一起,HTML 构成前端开发的三剑客。 尽管许多新技术使网站创建过程变得更简单、更高效,但 HTML 始终是核心...
继续阅读 »

HTML,超文本标记语言,是一种用于创建网页的标准标记语言。自从引入 HTML 以来,它就一直用于构建互联网。与 JavaScript 和 CSS 一起,HTML 构成前端开发的三剑客。


尽管许多新技术使网站创建过程变得更简单、更高效,但 HTML 始终是核心。随着 HTML5 的普及,在 2014 年,这种标记语言发生了很多变化,变得更加友好,浏览器对新标准的支持热度也越来越高。而HTML并不止于此,还在不断发生变化,并且可能会获得一些特性来证明对 HTML6 的命名更改是合理的。


支持原生模式


该元素<dialog> 将随 HTML6 一起提供。它被认为等同于用 JavaScript 开发的模态,并且已经标准化,但只有少数浏览器完全支持。但这种现象会改变,很快它将在所有浏览器中得到支持。


这个元素在其默认格式下,只会将光标显示在它所在的位置上,但可以使用 JavaScript 打开模式。


<dialog>
<form method="dialog">
<input type="submit" value="确定" />
<input type="submit" value="取消" />
</form>
</dialog>

在默认形式下,该元素创建一个灰色背景,其下方是非交互式内容。


可以在 <dialog> 其中的表单上使用一种方法,该方法将发送值并将其传递回自身 <dialog>


总的来说,这个标签在用户交互和改进的界面中变得有益。


可以通过更改 <dialog> 标签的 open 属性以控制打开和关闭。


<dialog open>
<p>组件内容</p>
</dialog>

没有 JavaScript 的单页应用程序


FutureClaw 杂志主编 Bobby Mozumder 建议:



将锚元素链接到 JSON/XML、API 端点,让浏览器在内部将数据加载到新的数据结构中,然后浏览器将 DOM 元素替换为根据需要加载的任何数据。初始数据(以及标准错误响应)可以放在标题装置中,如果需要,可以稍后替换。



据他介绍,这是单页应用程序网页设计模式,可以提高响应速度和加载时间,因为不需要加载 JavaScript。


自由调整图像大小


HTML6 爱好者相信即将到来的更新将允许浏览器调整图像大小以获得更好的观看体验。


每个浏览器都难以呈现相对于设备和屏幕尺寸的最佳图像尺寸,不幸的是,srce 标签 img 在处理这个问题时不是很有效。


这个问题可以通过一个新标签 <srcset> 来解决,它使浏览器在多个图像之间进行选择的工作变得更加容易。


专用库


将可用库引入 HTML6 绝对是提高开发效率的重要一步。


微格式


很多时候,需要在互联网上定义一般信息,而这些一般信息可以是任何公开的信息,例如电话号码、姓名、地址等。微格式是能够定义一般数据的标准。微格式可以增强设计者的能力,并可以减少搜索引擎推断公共信息所需的努力。


自定义菜单


尽管标签<ul><ol>非常有用,但在某些情况下仍有一些不足之处。可以处理交互元素的标签将是一个不错的选择。


这就是创建标签 <menu> 的驱动力,它可以处理按钮驱动的列表元素。


<menu type="toolbar">
<li><button>个人信息</button></li>
<li><button>系统设置</button></li>
<li><button>账号注销</button></li>
</menu>

因此 <menu>,除了能够像普通列表一样运行之外,还可以增强 HTML 列表的功能。


增强身份验证


虽然HTML5在安全性方面还不错,浏览器和网络技术也提供了合理的保护。毫无疑问,在身份验证和安全领域还有很多事情可以做。如密钥可以异地存储;这将防止不受欢迎的人访问并支持身份验证。使用嵌入式密钥而不是 cookie,使数字签名更好等。


集成摄像头


HTML6 允许以更好的方式使用设备上的相机和媒体。将能够控制相机、它的效果、模式、全景图像、HDR 和其他属性。


总结


没有什么是完美的,HTML 也不是完美的,所以 HTML 规范可以做很多事情来使它更好。应该对一些有用的规范进行标准化,以增强 HTML 的能力。小的变化已经开始推出。如增强蓝牙支持、p2p 文件传输、恶意软件保护、云存储集成,下一个 HTML 版本可以考虑一下。


作者:天行无忌
链接:https://juejin.cn/post/7032874253573685261

收起阅读 »

Flutter2.0快速体验写一个macos应用

1.简介 Flutter2.0将桌面端的开发支持加入到了stable分支中,这对于我一个移动开发小码农,产生了巨大的兴趣(/手动狗头),于是开始了我的第一个macos应用的开发(FTools),简单的说:开发桌面应用真的不要太简单了吧!下面是应用的截图,多图警...
继续阅读 »

1.简介


Flutter2.0将桌面端的开发支持加入到了stable分支中,这对于我一个移动开发小码农,产生了巨大的兴趣(/手动狗头),于是开始了我的第一个macos应用的开发(FTools),简单的说:开发桌面应用真的不要太简单了吧!下面是应用的截图,多图警告


2.屏幕截图



  • 明亮模式:







  • 深色模式:






3.MacOS应用开发


看到上面,是不是也是像我一样,想自己也写一个macos的工具应用,不要着急,下面来教大家如何创建和生成MacOS应用


1.配置环境


首先,确保你的FlutterSDK为2.0,我使用的是beta分支,也可以在stable分支下面查看到相同的版本号,至于Flutter的环境搭建,网上已经有很多相关的文章了,这里就直接省略了
image.png


2.配置可开发MacOS应用


运行下面命令即可


flutter config --enable-macos-deaktop

3.创建项目


我一般使用的是Android Studio,所以,按照步骤:


Create New Flutter Project


->选择 Flutter Application -> 点击 Next


->输入项目名Project Name -> 点击Next


->输入包名Package Name -> 点击Finish


-> 等待创建完毕(如果卡住了,可以试试设置代理,百度搜索:Flutter设置国内镜像)


-> 因为Android Studio 给我们创建的项目只能运行AndroidIOS,我们需要再命令行下切换到项目的根目录下,运行flutter create .命令即可,完成后,可以看到macos文件夹



4.运行项目


这里,我们需要给Android Studio 升级Flutter插件到最新的版本,然后选择macOS点击绿色三角按钮进行运行即可





4.FTools后续开发


这个应用目前只耗时了两天,后续还会继续维护并免费上架到AppStore,如果你想这个应用有哪些功能(用户面向于开发者),欢迎评论区留言给我,在能够实现并且时间充足的话会安排在开发计划当中。目前计划安排!



  1. Json To Table (JSON 转表格)

  2. Json To Create SQLite (JSON 转Sqlite创建)

  3. App Icon Make (应用图标制作)

  4. ...欢迎留言



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

Flutter 入门路线图

本文是为那些渴望开始学习 flutter 的人们而准备的,这是一个适合初学者从所有必要资源中逐步学习的路线图。 什么是 flutter Flutter 是 Google 的 UI 工具包,可通过单个代码库为移动设备,web 和桌面系统构建漂亮的,本机编译的应用...
继续阅读 »

本文是为那些渴望开始学习 flutter 的人们而准备的,这是一个适合初学者从所有必要资源中逐步学习的路线图。


什么是 flutter


Flutter 是 Google 的 UI 工具包,可通过单个代码库为移动设备,web 和桌面系统构建漂亮的,本机编译的应用程序。


下面两个视频很好地介绍了 flutter;


Introducing Flutter1


What's new in Flutter 20192



[1] https://youtu.be/fq4N0hgOWzU


[2] https://youtu.be/5VbAwhBBHsg


为什么是 flutter?


我们已经知道有很多框架可以提供跨平台功能,那么在这场激烈竞争中,是什么让 flutter 显得特别呢?


快速开发


Flutter 的热加载功能可帮助您快速轻松地进行实验,构建用户界面,添加功能并更快地修复错误。在 iOS 和 Android 的模拟器和硬件上体验亚秒级的重新加载时间,而不会丢失状态。


富有表现力的精美用户界面


Flutter 内置的精美 Material Design 和 Cupertino(iOS-flavor)小部件,丰富的运动 API,流畅的自然滚动以及对平台的了解,可为您的用户带来更多惊喜。


native 级别的性能


Flutter 的小部件结合了所有重要的平台差异,例如滚动,导航,图标和字体,以在 iOS 和 Android 上提供完整的 native 性能。


查看 Flutter 的功能


以下是全球开发人员构建的 Flutter 应用程序的展示。


Apps take flight with Flutter3


An open list of apps built with Flutter4


Flutter Awesome5


Start Flutter | Forever free, open source, and easy to use.6



[3]https://flutter.dev/showcase


[4]https://itsallwidgets.com/


[5]https://flutterawesome.com/


[6]https://startflutter.com/


首先要做什么?


Flutter 既快速又容易,如果您熟悉 Java 或任何面向对象的语言,那么很不错,但是我强烈建议您具备 Dart 的基本知识。


以下是一些可能对您有所帮助的视频。


Dart Programming for Flutter7


Dart Programming Tutorial - Full Course8


Introduction to Dart for Beginners9


Dart: Basics of Dart Part - 1/2 | Flutter10



[7]https://youtu.be/5rtujDjt50I?list=PLlxmoA0rQ-LyHW9voBdNo4gEEIh0SjG-q


[8]https://youtu.be/Ej_Pcr4uC2Q


[9]https://youtu.be/8F2uemqLwvE?list=PLJbE2Yu2zumDjfrfu8kisK9lQVcpMDDzZ


[10]https://youtu.be/DFRl4UyS7c8?list=PLR2qQy0Zxs_W4a6P70VYtzna7jwl3-lxI


对于那些不喜欢看视频的人,可以查看以下站点


Tutorials11


Dart Programming Tutorial12


Learn Dart In A Week With These Free Resources13



[11]https://dart.dev/tutorials


[12]https://www.tutorialspoint.com/dart_programming/index.htm


[13]https://hackernoon.com/learn-dart-in-a-week-with-these-free-resources-b892e5265220


是什么使 Dart 如此典型,为什么 flutter 会使用它?


为什么 Flutter 使用 Dart?


可以查看以下文章和视频


Why Flutter Uses Dart?14


视频:Why Flutter Uses Dart?15



[14]https://hackernoon.com/why-flutter-uses-dart-dd635a054ebf


[15]https://youtu.be/5F-6n_2XWR8


Flutter 底层是如何工作的?


由于 iOS 不允许动态编译,因此您的 Dart 代码会使用 AOT 直接编译为本地代码。


要了解更多信息,请在下面查看这些资源:


Technical overview16


How to Dart and Flutter Work Together?17


What's Revolutionary about Flutter18


How Flutter reners Widgets19


How is Flutter different for app development20



[16]https://flutter.dev/docs/resources/technical-overview


[17]https://youtu.be/iVYpeEd3Jes


[18]https://hackernoon.com/whats-revolutionary-about-flutter-946915b09514


[19]https://youtu.be/996ZgFRENMs


[20]https://youtu.be/l-YO9CmaSUM


Flutter快速且易于使用,现在让我们看看如何安装它。


如何安装Flutter?


这是开发人员文档的链接,您可以在其中找到在现有的操作系统中安装Flutter。


Install21



[21]https://flutter.dev/docs/get-started/install


解决安装过程中的问题


如果您在安装 flutter 时遇到任何问题,并且 flutter 无法正常工作,那么这就是出现了一些问题。


设置 flutter 路径时遇到麻烦-找不到flutter命令22


Flutter Doctor无法识别Android Studio flutter和dart插件,但已安装插件23


Flutter和Dart插件未在Flutter Doctor中安装警告24


安装 flutter 时的一些常见问题。25



[22]https://stackoverflow.com/questions/49268297/having-trouble-setting-flutter-path-flutter-commands-not-found


[23]https://github.com/flutter/flutter/issues/21881


[24]https://github.com/flutter/flutter/issues/11940


[25]https://github.com/flutter/flutter/wiki/Workarounds-for-common-issues


设置Flutter的编辑器


Set up an editor26



[26]https://flutter.dev/docs/get-started/editor


创建您的Flutter项目


通过以下命令创建 flutter 项目


flutter create <project-name>

或者您可以使用IDE(Intellij,Android Studio等)


项目概况


当您创建 flutter 应用程序时,您会看到这些文件和文件夹,大多数代码是用 dart 编写在 lib 文件夹中,native 代码放在 android 和 ios 目录下。


Jay Tillu的一篇文章解释了该项目的结构。


Flutter Project Structure27



[27]https://dev.to/jay_tillu/flutter-project-structure-1lhe


运行你的第一个 App


Test drive28


或者你可以使用以下命令来运行您的第一个应用程序


flutter run

当您启动第一个应用程序时,一定会感到很兴奋(从技术上说,这不是您的应用程序,代码已经在那里😜)。 我也很兴奋🎉。


创建flutter应用程序时,您会看到计数器应用程序已经有代码了。


运行代码时,您将看到此信息。这是一个简单的计数器应用程序,其中有一个FAB(FloatingActionButton)和 Text 来指示已按下 FAB 多少次。



[28]https://flutter.dev/docs/get-started/test-drive


flutter 中的 widget


如果看到代码,您将看到 StatefulWidget 和 StatelessWidget。在深入探讨之前,我们先来了解一下什么是 Widget。


Introduction to widget29


基本上,在 flutter 应用程序中看到的所有内容都是一个小部件。


我发现 What is a Widget in Flutter30 一文中的解释非常准确


Flutter小组还提供了一个YouTube播放列表(Widget of the week31),该列表仅讨论flutter中的Widget。



[29]https://flutter.dev/docs/development/ui/widgets-intro


[30]https://stackoverflow.com/questions/50958238/what-is-a-widget-in-flutter


[31]https://youtu.be/b_sQ9bMltGU?list=PLjxrf2q8roU23XGwz3Km7sQZFTdB996iG


什么是有状态和无状态小部件?


在 Stateless Widget 中,其所有属性都是不可变的,这意味着 StatelessWidget 永远不会自行重建(但可以从外部事件重建),而 StatefulWidget 可以。


Intro to Flutter - Stateful and Stateless Widgets, Widget Tree - Part One32


Flutter: Stateful vs Stateless Widget33


How to Create Stateless Widgets - Flutter Widgets 101 Ep. 134


How Stateful Widgets Are Used Best - Flutter Widgets 101 Ep. 235


Google's Flutter Tutorials | 6 - Stateless & Stateful Widgets | Android & iOS | Dart36



[32]https://youtu.be/-QRQIKtPTlI


[33]https://medium.com/flutter-community/flutter-stateful-vs-stateless-db325309deae


[34]https://www.youtube.com/watch?v=wE7khGHVkYY&feature=emb_title


[35]https://www.youtube.com/watch?v=AqCMFXEmf3w&feature=emb_title


[36]https://www.youtube.com/watch?list=PLR2qQy0Zxs_UdqAcaipPR3CG1Ly57UlhV&v=VnWHOogtDk8&feature=emb_title


让我们创建第一个Flutter应用


Google已经提供了一个 Codelab,您可以从那里开始学习如何构建自己的第一个 Flutter 应用程序。


Write Your First Flutter App, part 137


Write Your First Flutter App, part 238


Flutter Tutorial Part 1: Build a Flutter app from scratch39


1.3 Flutter Hello World Tutorial: Create First Flutter Application: Flutter Dart Tutorial40


1.4 First Flutter Application using Dart: PART-2 Flutter Tutorial for Beginners using Dart41



[37]https://codelabs.developers.google.com/codelabs/first-flutter-app-pt1/#4


[38]https://codelabs.developers.google.com/codelabs/first-flutter-app-pt2/#0


[39]https://medium.com/aviabird/flutter-tutorial-how-to-build-an-app-from-scratch-b88d4e0e10d7


[40]https://www.youtube.com/watch?list=PLlxmoA0rQ-Lw6tAs2fGFuXGP13-dWdKsB&v=dsyucuytW2k


[41]https://www.youtube.com/watch?list=PLlxmoA0rQ-Lw6tAs2fGFuXGP13-dWdKsB&v=ycHX8QtV08c


如何在 Flutter 中创建 UI?


为了使 UI 更加流畅,您需要基本了解布局以及如何使用它们。


Layouts in Flutter42


Flutter layout Cheat Sheet43



[42]https://flutter.dev/docs/development/ui/layout


[43]https://medium.com/flutter-community/flutter-layout-cheat-sheet-5363348d037e


如何在您的应用中添加交互?


在 flutter 中,您不能只是分配一个值并留下它


例如


String value="Hello";
------------------------------
Text(value);
---SOMEWHERE IN THE CODE------
onTap(){
value="How are you?";
}

如果您认为文本将要更改,那么您错了🙅‍♂️,您将不得不使用 setState()。


onTap(){
setState({
value="How are you?";
});
}

添加 setState() 将重建小部件并显示更改。


Adding interactivity to your Flutter app44


我建议您跟进有关开发的 Flutter 官方文档


Development45


flutter 中的所有内容都是小部件,您可以自行创建任何自定义小部件,但是已经有通过 flutter 定义的小部件。


Widget catalog46



[44]https://flutter.dev/docs/development/ui/interactive


[45]https://flutter.dev/docs/development


[46]https://flutter.dev/docs/development/ui/widgets


Flutter 中的 JSON 解析


JSON and serialization47


Parsing JSON in Flutter48


Parsing complex JSON in Flutter49


Working with APIs in Flutter50


Handling Network Calls like a Pro in Flutter51


Flutter - Build An App To Fetch Data Online Using HTTP GET | Android & iOS52


Testing, JSON serialization, and immutables (The Boring Flutter Development Show, Ep. 2)53



[47]https://flutter.dev/docs/development/data-and-backend/json


[48]https://medium.com/flutterdevs/parsing-complex-json-in-flutter-b7f991611d3e


[49]https://medium.com/flutter-community/parsing-complex-json-in-flutter-747c46655f51


[50]https://medium.com/flutter-community/working-with-apis-in-flutter-8745968103e9


[51]https://medium.com/flutter-community/handling-network-calls-like-a-pro-in-flutter-31bd30c86be1


[52]https://www.youtube.com/watch?list=PLR2qQy0Zxs_UdqAcaipPR3CG1Ly57UlhV&v=aIJU68Phi1w


[53]https://www.youtube.com/watch?v=TiCA0CEePyE


在 Flutter 中使用数据库


SQLite


Persist data with SQLite54


Data Persistence with SQLite | Flutter55


4.1 Flutter SQFLite Database Tutorial: Implement SQLite database with example: Section Overview56


Moor (Room for Flutter) #1 – Tables & Queries – Fluent SQLite Database57



[54]https://flutter.dev/docs/cookbook/persistence/sqlite


[55]https://medium.com/flutterdevs/data-persistence-with-sqlite-flutter-47a6f67b973f


[56]https://www.youtube.com/watch?list=PLDQl6gZtjvFu5l20K5KTEBLCjfRjHowLj&v=1BwjNEKD8g8


[57]https://www.youtube.com/watch?v=zpWsedYMczM


SharedPreferences


Shared preferences plugin58


Using SharedPreferences in Flutter59


Store key-value data on disk60


Making use of Shared Preferences, Flex Widgets and Dismissibles with Dart's Flutter framework61



[58]https://pub.dev/packages/shared_preferences


[59]https://medium.com/flutterdevs/using-sharedpreferences-in-flutter-251755f07127


[60]https://flutter.dev/docs/cookbook/persistence/key-value


[61]https://www.youtube.com/watch?v=IvrAAMQnj4k


使用Firebase


将 Firebase 添加到您的 Flutter 应用62


Firebase for Flutter63


Flutter - Firestore introduction64



[62]https://firebase.google.com/docs/flutter/setup


[63]https://codelabs.developers.google.com/codelabs/flutter-firebase/#0


[64]https://www.youtube.com/watch?list=PLgGjX33Qsw-Ha_8ks9im86sLIihimuYrr&v=LzEbpALmRlc


其他学习 Flutter 的资源


以下是其他开发人员和Flutter团队提供的一些资源:


Technical overview65


Resources to learn Flutter66


Free resources to learn and advance in Flutter67


Flutter Community68


My Favourite List of Flutter Resources69


awesome-flutter70


londonappbrewery/Flutter-Course-Resources71


A Searchable List of Flutter Resources72


FlutterDevs73



[65]https://flutter.dev/docs/resources/technical-overview


[66]https://medium.com/flutter-community/resources-to-learn-flutter-2ade7aa73305


[67]https://medium.com/flutter-community/free-resources-to-learn-and-advance-in-flutter-e07875ffc825


[68]https://medium.com/flutter-community


[69]https://medium.com/coding-with-flutter/my-favourite-list-of-flutter-resources-523adc611cbe


[70]https://github.com/Solido/awesome-flutter


[71]https://github.com/londonappbrewery/Flutter-Course-Resources


[72]https://flutterx.com/


[73]https://medium.com/flutterdevs


关于 Flutter 的问题


FAQ74


Answering Questions on Flutter App Development75


Flutter Vs. React Native: FAQs for Every Developer76



[74]https://flutter.dev/docs/resources/faq


[75]https://medium.com/@dev.n/answering-questions-on-flutter-app-development-6d50eb7223f3


[76]https://hackernoon.com/flutter-vs-react-native-faqs-for-every-developer-yjp329z


本文仅适用于初学者。


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

Kotlin + Flow 实现的 Android 应用初始化任务启动库

特性 Kotlin + Flow 实现的 Android 应用初始化任务启动库。 支持模块化,按模块加载任务 可指定工作进程名称,main 表示仅在主进程运行,all 表示在所有进程运行,默认值all 可指定任务仅在工作线程执行 可指定任务仅在调试模式执行 ...
继续阅读 »

特性


Kotlin + Flow 实现的 Android 应用初始化任务启动库。



  • 支持模块化,按模块加载任务

  • 可指定工作进程名称,main 表示仅在主进程运行,all 表示在所有进程运行,默认值all

  • 可指定任务仅在工作线程执行

  • 可指定任务仅在调试模式执行

  • 可指定任务在满足合规条件后执行

  • 可指定任务优先级,决定同模块内无依赖同步任务的执行顺序

  • 可指定依赖任务列表,能检测循环依赖

  • 使用 Flow 调度任务

  • 仅200多行代码,简单明了

  • 有耗时统计


引入依赖


项目地址:github.com/czy1121/ini…


repositories { 
maven { url "https://gitee.com/ezy/repo/raw/android_public/"}
}
dependencies {
implementation "me.reezy.init:init:0.9.0"
kapt "me.reezy.init:init-compiler:0.9.0"

// 使用 init-startup 代替 init 可以利用 Jetpack Startup 库自动初始化
// 无需在 Application.onCreate 调用 InitManager.init()
implementation "me.reezy.init:init-startup:0.9.0"
}

使用


AndroidManifest.xml<application> 里添加模块


<meta-data android:name="modules" android:value="app" />

通过注解 @InitInitTask 接口定义一个任务


@Init
class OneInit : InitTask {
override fun execute(app: Application) {
Log.e(TAG, "this is ${javaClass.simpleName} in ${Thread.currentThread().name}")
}
}

通过注解 @Init 的参数配置任务信息


@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class Init(
val process: String = "all", // 指定工作进程名称,main 表示仅在主进程运行,all 表示在所有进程运行
val background: Boolean = false, // 是否在工作线程执行任务
val debugOnly: Boolean = false, // 是否仅在 DEBUG 模式执行任务
val compliance: Boolean = false, // 是否需要合规执行
val depends: Array<String> = [], // 依赖的任务列表
val priority: Short = 0 //
)

APT会按模块收集任务信息并生成任务加载器(InitLoader_$moduleName),任务加载器用于添加任务到TaskList


class Task(
val name: String, // APT收集的任务名称格式为 "$moduleName:${clazz.simpleName}"
val background: Boolean = false, // 是否在工作线程执行任务
val priority: Int = 0, // 进程运行的优先级,值小的先执行
val depends: Set<String> = setOf(), // 依赖的任务列表,同模块只需指定"${clazz.simpleName}",跨模块需要指定 "$moduleName:${clazz.simpleName}"
val block: () -> Unit = {}, // 待执行的任务
) {
val children: MutableSet<Task> = mutableSetOf() // 子任务列表
}

核心类



  • TaskList 负责持有和添加任务

  • TaskManager 负责调度任务,支持添加开关任务(没有业务仅作为开关,可手动触发完成,并偿试执行其子任务)

    • 无依赖的异步任务,在子线程并行执行

    • 无依赖的同步任务,在主线程顺序执行

    • 有依赖的任务,确保无循环依赖,且被依赖的任务先执行



  • InitManager 负责找到各模块的任务加载器并开始启动初始化,它使用了一个合规开关来使相关任务在确定合规后执行


可以不使用 InitManager 收集任务


val taskList = TaskList(app).apply {
add("task1") {
}
add("task2", depends = setOf("t1")) {
}
add("task3", depends = setOf("task1")) {
}
}

val manager = TaskManager(taskList, setOf("t1"))
manager.start()

// ...

// 完成开关任务t1
manager.trigger("t1")

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

实现一套自己的WebKit

速度快:相比系统webview的网页打开速度有30+%的提升;省流量:使用云端优化技术使流量节省20+%;更安全:安全问题可以在24小时内修复;更稳定:经过亿级用户的使用考验,CRASH率低于0.15%;。。。打包chromium,生成官方的apk安装打开ch...
继续阅读 »

大家应该都听过腾讯的X5浏览器,据官方介绍其有以下优势:

  • 速度快:相比系统webview的网页打开速度有30+%的提升;

  • 省流量:使用云端优化技术使流量节省20+%;

  • 更安全:安全问题可以在24小时内修复;

  • 更稳定:经过亿级用户的使用考验,CRASH率低于0.15%;

  • 。。。

就不一一列举了,详细见(x5.tencent.com/docs/index.…
既然自定义webkit有能力做到这些,那我们为什么不自己试着整一个?

打包chromium,生成官方的apk

具体实现见Chromium网络请求

安装打开chrome_public_apk,它其实就是chrome浏览器app,那么我们怎么实现一套webkit呢?

webkit解构

webkit的包结构(从快手这看,容易理解)

这是快手app解开后webkit下的内容,整体上看,其实就是拷贝一套系统的webkit的api
上图1 mProvider就是webview的实现类,2 则为系统实现,既然是自定义webkit为什么要有2呢

主要应该是两个原因

  • 是chromium内核包太大了

  • 实现快速修复问题的目的,这可能就是X5提到的安全这点能在24小时之内修复

所以为了处理以上这两个问题,chromium内核一般都是插件化的实现,因此在插件还没有加载到的时候,我们只能去展示系统的webview,这里回到第一张图片,webkit adapter包下的就包含了所有webview参数和返回对象的包装,意在实现两套webkit对应的转换,比如

这里我们使用的地方调用的是com.kuaishou.webkit.CookieManager.getInstance(),在自己的内核没有加载完成的时候,所有调用都中转到了系统的android.webkit.CookieManager

难点攻坚

  • 内核模块调整,我们需要调整内核代码,把所有指向系统webkit包名全部改成我们自定义webkit的包名,为了使内核编译能通过,我们需要拷贝我们自定义的webkit这个模块到内核里去,在编译的时候把其剔除,这里我们需要去稍微了解一下gn构建配置相关的内容。

  • 插件化的实现,插件化现在应该都烂大街了,省略。

// 这里稍微提一下,加载内核apk的Classloader的实现
public static class DexClassLoaderOptimize extends DexClassLoader {
  @Override // java.lang.ClassLoader
  public Class loadClass(String str, boolean z) throws ClassNotFoundException {
      // 所有系统相关的类,或者我们的webkit层全部在我们app层找

      if (str.startsWith("java.") || ((str.startsWith("android.")
&& !str.startsWith("android.support.")) ||
str.startsWith("com.kuaishou.webkit"))) {

          return super.loadClass(str, z);
      }
      // 否则直接在插件中找
      try {
          return findClass(str);
      } catch (Exception unused) {
          return super.loadClass(str, z);
      }
  }
}
  • 整个webkit层,可以参照着快手的实现。

  • 在内核的加载过程中,主要涉及两个动态库一个是webview.so,另一个是webview_plat_support.so,其中webview_plat_support.so是Android系统内部的一个so,内核渲染的一个支持模块,我们需要使用到这个模块去做渲染的事情。(实际上Android10版本以上其实并不依赖该so去实现渲染能力)

webview_plat_support问题攻坚

webview_plat_support.so(以下简称plat_support)

一开始想过直接拷贝一份对应的so到我们的内核里,但是plat_support依赖了一些系统的动态库,api23以后ndk的动态库(见public),有些我们是引用不到的, 另外不同版本可能不同机型的plat_support的实现也有些差异,内核里需要维护太多的plat_support,增加复杂度

采用反射回调系统实现(所有内核里的native方法转到系统webview的实现),一开始在android10上测试ok,以为绕过了该问题,然而当我们在Android10以下的版本上,同时开启系统WebView和自定义webview就出现问题了,运气好的话要么系统WebView黑屏,要么自定义的WebView黑屏,多数情况下会崩溃,主要是plat_support其中一个方法nativeSetChromiumAwDrawGLFunction,保存chromium 内部渲染出口对象是一个单例实现,所以会出现两边同时都调用到这个方法,要么系统WebView黑屏,要么系统自定义Webview黑屏,所以我们需要自己实现一个plat_support,一开始参考快手的实现,因为从第三个图中,我们看到它有对应的plat_support

同时还看到了,它对系统库libskia.so的调用

实现上主要是GraphicsUtils对应的两个native方法比较复杂涉及到GraphicBuffer类 GraphicBuffer这个类位于libui.so,然而快手的plat_support中并没有找到libui.so的调用,既然不引用libui.so,我们就要自己去创建对应的对象,这里我们可以把它改成结构体

匹配所有的成员变量,就可以强转成目标对象(这和java完全不一样,一般没有关联的类的实例不能强转成目标类,不能调用的目标类的方法),只是我们需要该类原本实现的方法在本地重新实现一次,然而目标类的方法里又有别的类的调用,延展开来,最终处理的内容就很多了,还有一大堆版本适配问题,到这里暂时hold。

好了那么我们再来看看X5的实现,

好家伙,这直接反回0,然后我立马测试了一下,通过~~~
也就是说前面海量的代码实现,其实是可以删除的,chromium内部有自己默认的实现(这里我们还是可以反射转回到系统webview的实现,包括nativeGetFunctionTable)。

剩下的我们只需处理以下几个native方法即可 这个几个方法处理就非常简单了,注意kAwDrawGLInfoVersion的版本号适配就Ok了。

结语

当我们整完这一套,就可以开始我们的内核定制了,包括小程序领域所谓的同城渲染能力,JS ServerWorker的定制等等。

作者:北纬34点8度
来源:https: //juejin.cn/post/7037807249103798285

收起阅读 »

先睹为快即将到来的HTML6

HTML,超文本标记语言,是一种用于创建网页的标准标记语言。自从引入 HTML 以来,它就一直用于构建互联网。与 JavaScript 和 CSS 一起,HTML 构成前端开发的三剑客。 尽管许多新技术使网站创建过程变得更简单、更高效,但 HTML 始终是核...
继续阅读 »
HTML,超文本标记语言,是一种用于创建网页的标准标记语言。自从引入 HTML 以来,它就一直用于构建互联网。与 JavaScript 和 CSS 一起,HTML 构成前端开发的三剑客。

尽管许多新技术使网站创建过程变得更简单、更高效,但 HTML 始终是核心。随着 HTML5 的普及,在 2014 年,这种标记语言发生了很多变化,变得更加友好,浏览器对新标准的支持热度也越来越高。而HTML并不止于此,还在不断发生变化,并且可能会获得一些特性来证明对 HTML6 的命名更改是合理的。

支持原生模式

该元素<dialog> 将随 HTML6 一起提供。它被认为等同于用 JavaScript 开发的模态,并且已经标准化,但只有少数浏览器完全支持。但这种现象会改变,很快它将在所有浏览器中得到支持。

这个元素在其默认格式下,只会将光标显示在它所在的位置上,但可以使用 JavaScript 打开模式。

<dialog>
<form method="dialog">
  <input type="submit" value="确定" />
  <input type="submit" value="取消" />
</form>
</dialog>

在默认形式下,该元素创建一个灰色背景,其下方是非交互式内容。

可以在 <dialog> 其中的表单上使用一种方法,该方法将发送值并将其传递回自身 <dialog>

总的来说,这个标签在用户交互和改进的界面中变得有益。

可以通过更改 <dialog> 标签的 open 属性以控制打开和关闭。

<dialog open>
<p>组件内容</p>
</dialog>

没有 JavaScript 的单页应用程序

FutureClaw 杂志主编 Bobby Mozumder 建议:

将锚元素链接到 JSON/XML、API 端点,让浏览器在内部将数据加载到新的数据结构中,然后浏览器将 DOM 元素替换为根据需要加载的任何数据。初始数据(以及标准错误响应)可以放在标题装置中,如果需要,可以稍后替换。

据他介绍,这是单页应用程序网页设计模式,可以提高响应速度和加载时间,因为不需要加载 JavaScript。

自由调整图像大小

HTML6 爱好者相信即将到来的更新将允许浏览器调整图像大小以获得更好的观看体验。

每个浏览器都难以呈现相对于设备和屏幕尺寸的最佳图像尺寸,不幸的是,srce 标签 img 在处理这个问题时不是很有效。

这个问题可以通过一个新标签 <srcset> 来解决,它使浏览器在多个图像之间进行选择的工作变得更加容易。

专用库

将可用库引入 HTML6 绝对是提高开发效率的重要一步。

微格式

很多时候,需要在互联网上定义一般信息,而这些一般信息可以是任何公开的信息,例如电话号码、姓名、地址等。微格式是能够定义一般数据的标准。微格式可以增强设计者的能力,并可以减少搜索引擎推断公共信息所需的努力。

自定义菜单

尽管标签<ul><ol>非常有用,但在某些情况下仍有一些不足之处。可以处理交互元素的标签将是一个不错的选择。

这就是创建标签 <menu> 的驱动力,它可以处理按钮驱动的列表元素。

<menu type="toolbar">
<li><button>个人信息</button></li>
<li><button>系统设置</button></li>
<li><button>账号注销</button></li>
</menu>

因此 <menu>,除了能够像普通列表一样运行之外,还可以增强 HTML 列表的功能。

增强身份验证

虽然HTML5在安全性方面还不错,浏览器和网络技术也提供了合理的保护。毫无疑问,在身份验证和安全领域还有很多事情可以做。如密钥可以异地存储;这将防止不受欢迎的人访问并支持身份验证。使用嵌入式密钥而不是 cookie,使数字签名更好等。

集成摄像头

HTML6 允许以更好的方式使用设备上的相机和媒体。将能够控制相机、它的效果、模式、全景图像、HDR 和其他属性。

总结

没有什么是完美的,HTML 也不是完美的,所以 HTML 规范可以做很多事情来使它更好。应该对一些有用的规范进行标准化,以增强 HTML 的能力。小的变化已经开始推出。如增强蓝牙支持、p2p 文件传输、恶意软件保护、云存储集成,下一个 HTML 版本可以考虑一下。

作者:天行无忌
来源:https://juejin.cn/post/7032874253573685261

收起阅读 »

Redis分布式锁

需求分布式应⽤进⾏逻辑处理时经常会遇到并发问题。互斥访问某个网络上的资源,需要有一个存在于网络上的锁服务器,负责锁的申请与回收。Redis 可以充当锁服务器的角色。首先,Redis 是单进程单线程的工作模式,所有前来申请锁资源的请求都被排队处理,能保证锁资源的...
继续阅读 »



需求

分布式应⽤进⾏逻辑处理时经常会遇到并发问题。

互斥访问某个网络上的资源,需要有一个存在于网络上的锁服务器,负责锁的申请与回收。Redis 可以充当锁服务器的角色。首先,Redis 是单进程单线程的工作模式,所有前来申请锁资源的请求都被排队处理,能保证锁资源的同步访问。

适用原因:

  • Redis 可以被多个客户端共享访问,是·共享存储系统,可以用来保存分布 式锁

  • Redis 的读写性能高,可以应对高并发的锁操作场景。

实现

在分布式场景下,锁变量需要由一个共享存储系统来维护,这样,多个客户端可以通过访问共享存储系统来访问锁变量。

简单实现

模仿单机上的锁,使用锁变量即可在Redis上实现分布式锁。

我们可以在 Redis 服务器设置一个键值对,用以表示一把互斥锁,当申请锁的时候,要求申请方设置(SET)这个键值对,当释放锁的时候,要求释放方删除(DEL)这个键值对。

但最基本需要保证加锁解锁操作的原子性。同时为了保证锁在异常情况下能被释放,必须设置超时时间。

Redis 2.8 版本中作者加⼊了 set 指令的扩展参数,使得 setnx 和 expire 指令可以⼀起执⾏

加锁原子操作

加锁包含了三个操作(读取锁变量、判断锁变量值以及把锁变量值设置为 1),而这 三个操作在执行时需要保证原子性。

首先是 SETNX 命令,它用于设置键值对的值。具体来说,就是这个命令在执行时会判断键 值对是否存在,如果不存在,就设置键值对的值,如果存在,就不做任何设置。

超时问题

Redis 的分布式锁不能解决超时问题,如果在加锁和释放锁之间的逻 辑执⾏的太长,超出了锁的超时限制,就无法保证互斥。

解决方案

最简单的就是避免Redis 分布式锁⽤于较⻓时间的任务。如果真的偶尔出现了,数据出现的⼩波错乱可能需要⼈⼯介⼊解决。

判断拥有者

为了防止锁变量被拥有者之外的客户端进程删除,需要能区分来自不同客户端的锁操作

set 指令的 value 参数设置为⼀个 随机数,释放锁时先匹配随机数是否⼀致,然后再删除 key,这是为 了确保当前线程占有的锁不会被其它线程释放,除⾮这个锁是过期了被服务器⾃动释放的。

Redis 给 SET 命令提供 了类似的选项 NX,用来实现“不存在即设置”。如果使用了 NX 选项,SET 命令只有在键 值对不存在时,才会进行设置,否则不做赋值操作。此外,SET 命令在执行时还可以带上 EX 或 PX 选项,用来设置键值对的过期时间。

可重入问题

可重⼊性是指线程在持有锁的情况下再次请求加锁,如果⼀个锁⽀持 同⼀个线程的多次加锁,那么这个锁就是可重⼊的。Redis 分布式锁如果要⽀持 可重⼊,需要对客户端的 set ⽅法进⾏包装,使⽤线程的 Threadlocal 变量存储当前持有锁的计数。

分布式拓展

单Redis实例并不能满足我们的高可用要求,一旦实例崩溃,就无法对外分布式锁服务。

但在集群环境下,这种只对主Redis实例使用上述方案是有缺陷 的,它不是绝对安全的。

一旦主节点挂掉,但锁变量没有及时同步,就会导致互斥被破坏。

Redlock

为了解决这个问题,Antirez 发明了 Redlock 算法

Redlock 算法的基本思路,是让客户端和多个独立的 Redis 实例依次请求加锁,如果客户 端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布 式锁了,否则加锁失败。

执行步骤:

  • 获取当前时间

  • 客户端按顺序依次向 N 个 Redis 实例执行加锁操作

  • 一旦客户端完成了和所有 Redis 实例的加锁操作,客户端就要计算整个加锁过 程的总耗时。客户端只有在满足下面的这两个条件时,才能认为是加锁成功

    • 客户端从超过半数(大于等于 N/2+1)的 Redis 实例上成功获取到了锁;

    • 客户端获取锁的总耗时没有超过锁的有效时间。


作者:不是二向箔
来源:https://juejin.cn/post/7038155529566289957

收起阅读 »

是时候封装一个DOM库了

增首先,如果用原始的DOM API,我们想要创建一个div,div里面含有一个文本'hi',需要分为两步而这里,只需要一步就能完成dom.create('hi') 它可以直接创建多标签的嵌套,如create('你好') 为什么能这样写? 因为我们用inn...
继续阅读 »

由于原始的DOM提供的API过长,不方便记忆,
于是我采用对象风格的形式封装了一个DOM库->源代码链接
这里对新封装的API进行总结
同样,用增删改查进行划分,我们先提供一个全局的window.dom对象

创建节点

create (string){
       const container = document.createElement("template")//template可以容纳任意元素
       container.innerHTML = string.trim();//去除字符串两边空格
       return container.content.firstChild;//用template,里面的元素必须这样获取  
}

首先,如果用原始的DOM API,我们想要创建一个div,div里面含有一个文本'hi',需要分为两步

  1. document.createElement('div')

  2. div.innerText = 'hi'

而这里,只需要一步就能完成dom.create('

hi
')
它可以直接创建多标签的嵌套,如create('
你好
')

为什么能这样写?
因为我们用innerHTML直接把字符串写进了HTML里,字符串直接变成了HTML里面的内容
为什么使用template?
因为template可以容纳任意元素,如果使用div,div不能直接容纳标签,但template就可以

新增哥哥

before(node,node2){
   node.parentNode.insertBefore(node2,node);
}

这个比较简单,找到爸爸节点,然后使用```JavaScript,新增一个node2即可

新增弟弟

after(node,node2){
   node.parentNode.insertBefore(node2,node.nextSibling); //把node2插到node下一个节点的前面,即使node的下一个节点为空,也能插入  
}

由于原始的DOM只有insertBefore,并没有insertAfter,所以要实现这个功能我们需要一个曲线救国的方法:
node.nextSibling 表示node节点的下一个节点,
而想在node的后面插入一个节点,就等于说在node的下一个节点前插入一个新节点node2即可
如上代码就是实现了这个操作,而即使node的下一个节点为空,也能成功插入

新增儿子

 append(parent,node){
       parent.appendChild(node)
}

找到爸爸节点,用appendChild即可

新增爸爸

 wrap(node,parent){
       dom.before(node,parent)
       dom.append(parent,node)
}

思路如图:

image.png

分为两步走:

  1. 先把新增的爸爸节点,放到老节点的前面

  2. 再把老节点放入新增的爸爸节点的里面

这样就可以使新的爸爸节点包裹住老节点

使用示例:

const newDiv = dom.create('
'
)

dom.wrap(test, newDiv)

删节点

remove(node){
      node.parentNode.removeChild(node)
      return node
}

找到爸爸节点,removeChild即可

删除所有子节点

empty(node){
   const array = []
   let x = node.firstChild
   while (x) {
       array.push(dom.remove(node.firstChild))
       x = node.firstChild//x指向下一个节点
  }
   return array
}

其实一开始的思路,是用for循环

for(let i = 0;i<childNodes.length;i++){
   dom.remove(childNodes[i])
}

但这样的思路有一个问题:childNodes.length是会随着删除而变化的
所以我们需要改变思路,用while循环:

  1. 先找到该节点的第一个儿子赋值为x

  2. 当x是存在的,我们就把它移除,并放入数组里面(用于获取删除的节点的引用)

  3. 再把x赋值给它的下一个节点(当第一个儿子被删除后,下一个儿子就变成了第一个儿子)

  4. 反复操作,直到所有子节点被删完

读写属性

 attr(node,name,value){
       if(arguments.length === 3){
           node.setAttribute(name,value)
      }else if(arguments.length === 2){
           return node.getAttribute(name)
      }
}

这里运用重载,实现两种不同的功能:

  1. 当输入的参数是3个时,就写属性

  2. 当如数的参数是2个时,读属性

使用示例:

//写:
//给
test
添加属性

dom.attr(test,'title','Hi,I am Wang')
//添加之后:
test

//读:
const title = dom.attr(test,'title')
console.log(`title:${title}`)
//打印出:title:Hi,Hi,I am Wang

读写文本内容

text(node,string){
   if(arguments.length === 2){
       if('innerText' in node){
           node.innerText = string
      }else{
           node.textContent = string
      }
  }else if(arguments.length === 1){
       if('innerText' in node){
           return node.innerText
      }else{
           return node.textContent
      }
  }
}

为什么这里需要适配,innerText与textContent?
因为虽然现在绝大多数浏览器都支持两种,但还是有非常旧的IE只支持innerText,所以这里是为了适配所有浏览器

同时与读写属性思路相同:

  1. 当输入的参数是2个时,就在节点里写文本

  2. 当输入的参数是1个时,就读文本内容

读写HTML的内容

html(node,string){
   if(arguments.length === 2){
       node.innerHTML = string
  }else if(arguments.length === 1){
           return node.innerHTML
  }
}

同样,2参数写内容,1参数读内容

修改Style

style(node,name,value){
       if(arguments.length === 3){
           //dom.style(div,'color','red')
           node.style[name] = value
      }else if(arguments.length === 2){
           if(typeof name === 'string'){
            //dom.style(div,'color')
           return node.style[name]    
          }else if(name instanceof Object){
               //dom.style(div,{color:'red'})
               const Object = name
               for(let key in Object){
                   //key:border/color
                   //node.style.border = ...
                   //node.style.color = ...
                   node.style[key] = Object[key]
              }
          }
      }
}

思路:

  1. 首先判断输入的参数,如果为3个如:dom.style(div,'color','red')

  2. 就更改它的style

  3. 如果输入参数为2个时,先判断输入name的值的类型

  4. 如果是字符串,如dom.style(div,'color'),就返回style的属性

  5. 如果是对象,如dom.style(div,{border:'1px solid red',color:'blue'}),就更改它的style

增删查class

class:{
   add(node,className){
       node.classList.add(className)    
  },
   remove(node,className){
       node.classList.remove(className)
  },
   has(node,className){
       return node.classList.contains(className)
  }
}

注:查找一个元素的classList里是否有某一个class, 用的是contains

添加事件监听

on(node,eventName,fn){
      node.addEventListener(eventName,fn)
}

使用示例:

const fn = ()=>{
   console.log('点击了')
}
dom.on(test,'click',fn)

这样当点击id为test的div时,就会打印出'点击了'

删除事件监听

off(node,eventName,fn){
   node.removeEventListener(eventName,fn)
}

获取单个或多个标签

find(selector,scope){
   return (scope || document).querySelectorAll(selector)
}

可以在指定区域或者全局的document里找

使用示例:
在document中查询:

const testDiv = dom.find('#test')[0]
console.log(testDiv)

在指定范围内查询:

 <div>
       <div id="test"><span>test1span>
       <p class="red">段落标签p>
       div>
       <div id="test2">
           <p class="red">段落标签p>
       div>
div>

我只想找test2里面的red,应该怎么做

const test2 = dom.find('#test2')[0]
console.log(dom.find('.red',test2)[0])

注意:末尾的[0]别忘记写

获取父元素

parent(node){
   return node.parentNode
}

获取子元素

children(node){
   return node.children
}

获取兄弟姐妹元素

siblings(node){
   return Array.from(node.parentNode.children).filter(n=>n!==node) //伪数组变数组再过滤本身
}

找到爸爸节点,然后过滤掉自己本身

获取弟弟

next(node){
   let x = node.nextSibling
   while(x && x.nodeType === 3){
       x = x.nextSibling
  }
   return x
}

为什么这里需要while(x && x.nodeType === 3)
因为我们不想获取文本节点(空格回车等)
所以当读到文本节点时,自动再去读取下一个节点,直到读到的内容不是文本节点为止

获取哥哥

previous(node){
       let x = node.previousSibling
       while(x && x.nodeType === 3){
           x = x.previousSibling
      }
       return x
}

与上面思路相同

遍历所有节点

each(nodeList,fn){
       for(let i=0;i<nodeList.length;i++){
           fn.call(null,nodeList[i])
      }
}

注:null用于填充this的位置
使用示例:
利用fn可以更改所有节点的style

const t = dom.find('#travel')[0]
dom.each(dom.children(t),(n)=>dom.style(n,'color','red'))

遍历每个节点,把每个节点的style都更改

用于获取排行老几

index(node){
   const list = dom.children(node.parentNode)
   let i;
   for(i=0;i<list.length;i++){
       if(list[i]===node){
            break
      }
  }
   return i
}

思路:

  1. 获取爸爸节点的所有儿子

  2. 设置一个变量i

  3. 如果i等于想要查询的node

  4. 退出循环,返回i值

作者:PrayWang
来源:https://juejin.cn/post/7038171258617331719

收起阅读 »

这一次,彻底搞懂 async...await

执行 async 函数,返回的都是 Promise 对象Promise.then() 对应 awaitPromise.catch() 对应 try...catch先看下面两个函数:async function test1() {  return 1;...
继续阅读 »

先上结论:

  • 执行 async 函数,返回的都是 Promise 对象

  • Promise.then() 对应 await

  • Promise.catch() 对应 try...catch

执行 async 函数,返回的都是 Promise 对象

先看下面两个函数:

async function test1() {
 return 1;
}
async function test2() {
 return Promise.resolve(2);
}
const res1 = test1();
const res2 = test2();
console.log('res1', res1);
console.log('res1', res2);

test1 和 test2 两个函数前面都加了 async,说明这两个都是异步函数,并且如果一个函数前加了 async,那么这个函数的返回值就是一个 Promise(不论这个函数返回的是什么,都会被 JS 引擎包装成 Promise 对象)。

输出结果如下图:

Promise.then() 对应 await

1.直接在一个 await 后面加 promise 对象

看下面的代码:

async function test3() {
 const p3 = Promise.resolve(3);
 p3.then(data => {
   console.log('data', data);
});

 const data = await p3;
 console.log('data', data);
}
test3();

输出结果如下图:

可以看到输出是相同的,这就说明了 Promise 的 then() 方法对应 await。

2.直接在一个 await 后面加一个基本数据类型的值

看下面的例子:

async function test4() {
 const data4 = await 4; // await Promise.resolve(4)
 console.log('data4', data4);
}
test4();

输出结果如下图:

可以看到输出的是 4,上面的 await 4 就相当于 await Promise.resolve(4),又因为 await 相当于 then(),所以输出的就是 4。

3.直接在一个 await 后面加一个异步函数

看下面的例子:

async function test1() {
 return 1;
}
async function test5() {
 const data5 = await test1();
 console.log('data5', data5);
}
test5();

输出结果如下图:

可以看到输出的是 1,首先 test5() 执行,然后执行 test1(),test1 返回数字 1,相当于返回 Promise.resolve(1),await 又相当于 then(),所以输出 1。

:::tip 提示 开发中最常用的就是第三种,await 后面跟一个异步函数,所以一定要掌握! :::

Promise.catch() 对应 try...catch

看下面的例子:

async function test6() {
 const p6 = Promise.reject(6);
 const data6 = await p6;
 console.log('data6', data6);
}
test6();

输出结果如下图:

可以看到没有捕获到错误,那应该怎么做呢?没错,可以使用 try...catch。 看下面的例子:

async function test6() {
 const p6 = Promise.reject(6);
 try {
   const data6 = await p6;
   console.log('data6', data6);
} catch (e) {
   console.log('e', e); // 顺利捕获错误
}
}
test6();

输出结果如下图:

可以看到已经成功捕获到错误了!

作者:ShiYan_Chen
来源:https://juejin.cn/post/7038152028664627230

收起阅读 »

女友半夜加班发自拍 python男友用30行代码发现惊天秘密

事情是这样的接到女朋友今晚要加班的电话如下 ↓ ↓ ↓敏感的小哥哥心生疑窦,难道会有原谅帽然后python撸了一段代码 分析照片小哥哥崩溃之余 大呼上当小哥哥将发给自己的照片原图下载下来并使用python写了一个脚本读取到了照片拍摄的详细的地址详细到了具体的街...
继续阅读 »



事情是这样的

正准备下班的python开发小哥哥

接到女朋友今晚要加班的电话

并给他发来一张背景模糊的自拍照

如下 ↓ ↓ ↓

敏感的小哥哥心生疑窦,难道会有原谅帽
然后python撸了一段代码 分析照片

分析下来 emmm
拍摄地址居然在 XXX酒店

小哥哥崩溃之余 大呼上当

python分析照片

小哥哥将发给自己的照片原图下载下来
并使用python写了一个脚本
读取到了照片拍摄的详细的地址
详细到了具体的街道和酒店名称

引入exifread模块

首先安装python的exifread模块,用于照片分析
pip install exifread 安装exfriead模块

PS C:\WINDOWS\system32> pip install exifread
Collecting exifread
Downloading ExifRead-2.3.2-py3-none-any.whl (38 kB)
Installing collected packages: exifread
Successfully installed exifread-2.3.2
PS C:\WINDOWS\system32> pip install json

GPS经纬度信息

其实我们平时拍摄的照片里,隐藏了大量的私密信息
包括 拍摄时间、极其精确 具体的GPS信息。
下面是通过exifread模块,来读取照片内的经纬度信息。

#读取照片的GPS经纬度信息
def find_GPS_image(pic_path):
GPS = {}
date = ''
with open(pic_path, 'rb') as f:
tags = exifread.process_file(f)
for tag, value in tags.items():
#纬度
if re.match('GPS GPSLatitudeRef', tag):
GPS['GPSLatitudeRef'] = str(value)
#经度
elif re.match('GPS GPSLongitudeRef', tag):
GPS['GPSLongitudeRef'] = str(value)
#海拔
elif re.match('GPS GPSAltitudeRef', tag):
GPS['GPSAltitudeRef'] = str(value)
elif re.match('GPS GPSLatitude', tag):
try:
match_result = re.match('\[(\w*),(\w*),(\w.*)/(\w.*)\]', str(value)).groups()
GPS['GPSLatitude'] = int(match_result[0]), int(match_result[1]), int(match_result[2])
except:
deg, min, sec = [x.replace(' ', '') for x in str(value)[1:-1].split(',')]
GPS['GPSLatitude'] = latitude_and_longitude_convert_to_decimal_system(deg, min, sec)
elif re.match('GPS GPSLongitude', tag):
try:
match_result = re.match('\[(\w*),(\w*),(\w.*)/(\w.*)\]', str(value)).groups()
GPS['GPSLongitude'] = int(match_result[0]), int(match_result[1]), int(match_result[2])
except:
deg, min, sec = [x.replace(' ', '') for x in str(value)[1:-1].split(',')]
GPS['GPSLongitude'] = latitude_and_longitude_convert_to_decimal_system(deg, min, sec)
elif re.match('GPS GPSAltitude', tag):
GPS['GPSAltitude'] = str(value)
elif re.match('.*Date.*', tag):
date = str(value)
return {'GPS_information': GPS, 'date_information': date}

百度API将GPS转地址

这里需要使用调用百度API,将GPS经纬度信息转换为具体的地址信息。

这里,你需要一个调用百度API的ak值,这个可以注册一个百度开发者获得,当然,你也可以使用博主的这个ak

调用之后,就可以将拍摄时间、拍摄详细地址都解析出来。

def find_address_from_GPS(GPS):
secret_key = 'zbLsuDDL4CS2U0M4KezOZZbGUY9iWtVf'
if not GPS['GPS_information']:
return '该照片无GPS信息'
#经纬度信息
lat, lng = GPS['GPS_information']['GPSLatitude'], GPS['GPS_information']['GPSLongitude']
baidu_map_api = "http://api.map.baidu.com/geocoder/v2/?ak={0}&callback=renderReverse&location={1},{2}s&output=json&pois=0".format(
secret_key, lat, lng)
response = requests.get(baidu_map_api)
#百度API转换成具体的地址
content = response.text.replace("renderReverse&&renderReverse(", "")[:-1]
print(content)
baidu_map_address = json.loads(content)
#将返回的json信息解析整理出来
formatted_address = baidu_map_address["result"]["formatted_address"]
province = baidu_map_address["result"]["addressComponent"]["province"]
city = baidu_map_address["result"]["addressComponent"]["city"]
district = baidu_map_address["result"]["addressComponent"]["district"]
location = baidu_map_address["result"]["sematic_description"]
return formatted_address,province,city,district,location

if __name__ == '__main__':
GPS_info = find_GPS_image(pic_path='C:/女友自拍.jpg')
address = find_address_from_GPS(GPS=GPS_info)
print("拍摄时间:" + GPS_info.get("date_information"))
print('照片拍摄地址:' + str(address))

老王得到的结果是这样的

照片拍摄地址:('云南省红河哈尼族彝族自治州弥勒县', '云南省', '红河哈尼族彝族自治州', '弥勒县', '湖泉酒店-A座东南128米')

云南弥勒湖泉酒店,这明显不是老王女友工作的地方,老王搜索了一下,这是一家温泉度假酒店。

顿时就明白了

{"status":0,"result":{"location":{"lng":103.41424699999998,"lat":24.410461020097278},
"formatted_address":"云南省红河哈尼族彝族自治州弥勒县",
"business":"",
"addressComponent":{"country":"中国",
"country_code":0,
"country_code_iso":"CHN",
"country_code_iso2":"CN",
"province":"云南省",
"city":"红河哈尼族彝族自治州",
"city_level":2,"district":"弥勒县",
"town":"","town_code":"","adcode":"532526",
"street_number":"",
"direction":"","distance":""},
"sematic_description":"湖泉酒店-A座东南128米",
"cityCode":107}}

拍摄时间:2021:5:03 20:05:32
照片拍摄地址:('云南省红河哈尼族彝族自治州弥勒县', '云南省', '红河哈尼族彝族自治州', '弥勒县', '湖泉酒店-A座东南128米')

完整代码如下

import exifread
import re
import json
import requests
import os

#转换经纬度格式
def latitude_and_longitude_convert_to_decimal_system(*arg):
"""
经纬度转为小数, param arg:
:return: 十进制小数
"""
return float(arg[0]) + ((float(arg[1]) + (float(arg[2].split('/')[0]) / float(arg[2].split('/')[-1]) / 60)) / 60)

#读取照片的GPS经纬度信息
def find_GPS_image(pic_path):
GPS = {}
date = ''
with open(pic_path, 'rb') as f:
tags = exifread.process_file(f)
for tag, value in tags.items():
#纬度
if re.match('GPS GPSLatitudeRef', tag):
GPS['GPSLatitudeRef'] = str(value)
#经度
elif re.match('GPS GPSLongitudeRef', tag):
GPS['GPSLongitudeRef'] = str(value)
#海拔
elif re.match('GPS GPSAltitudeRef', tag):
GPS['GPSAltitudeRef'] = str(value)
elif re.match('GPS GPSLatitude', tag):
try:
match_result = re.match('\[(\w*),(\w*),(\w.*)/(\w.*)\]', str(value)).groups()
GPS['GPSLatitude'] = int(match_result[0]), int(match_result[1]), int(match_result[2])
except:
deg, min, sec = [x.replace(' ', '') for x in str(value)[1:-1].split(',')]
GPS['GPSLatitude'] = latitude_and_longitude_convert_to_decimal_system(deg, min, sec)
elif re.match('GPS GPSLongitude', tag):
try:
match_result = re.match('\[(\w*),(\w*),(\w.*)/(\w.*)\]', str(value)).groups()
GPS['GPSLongitude'] = int(match_result[0]), int(match_result[1]), int(match_result[2])
except:
deg, min, sec = [x.replace(' ', '') for x in str(value)[1:-1].split(',')]
GPS['GPSLongitude'] = latitude_and_longitude_convert_to_decimal_system(deg, min, sec)
elif re.match('GPS GPSAltitude', tag):
GPS['GPSAltitude'] = str(value)
elif re.match('.*Date.*', tag):
date = str(value)
return {'GPS_information': GPS, 'date_information': date}

#通过baidu Map的API将GPS信息转换成地址。
def find_address_from_GPS(GPS):
"""
使用Geocoding API把经纬度坐标转换为结构化地址。
:param GPS:
:return:
"""
secret_key = 'zbLsuDDL4CS2U0M4KezOZZbGUY9iWtVf'
if not GPS['GPS_information']:
return '该照片无GPS信息'
lat, lng = GPS['GPS_information']['GPSLatitude'], GPS['GPS_information']['GPSLongitude']
baidu_map_api = "http://api.map.baidu.com/geocoder/v2/?ak={0}&callback=renderReverse&location={1},{2}s&output=json&pois=0".format(
secret_key, lat, lng)
response = requests.get(baidu_map_api)
content = response.text.replace("renderReverse&&renderReverse(", "")[:-1]
print(content)
baidu_map_address = json.loads(content)
formatted_address = baidu_map_address["result"]["formatted_address"]
province = baidu_map_address["result"]["addressComponent"]["province"]
city = baidu_map_address["result"]["addressComponent"]["city"]
district = baidu_map_address["result"]["addressComponent"]["district"]
location = baidu_map_address["result"]["sematic_description"]
return formatted_address,province,city,district,location
if __name__ == '__main__':
GPS_info = find_GPS_image(pic_path='C:/Users/pacer/desktop/img/5.jpg')
address = find_address_from_GPS(GPS=GPS_info)
print("拍摄时间:" + GPS_info.get("date_information"))
print('照片拍摄地址:' + str(address))

作者:LexSaints
来源:https://juejin.cn/post/6967563349609414692

收起阅读 »

会话过期后token刷新,重新请求接口(订阅发布模式)

需求响应拦截拦截到302后,我们进入到刷新token逻辑我们后台的数据格式是根据statusCode来判断过期(你们可以根据自己的实际情况判断),接着进入refrshToken方法~看到这,有的小伙伴就有点奇怪retryOldRequest这个又是什么?没错,...
继续阅读 »

前言


❝ 最近,我们老大让小白搞一下登录模块,登录模块说简单也简单,复杂也复杂。本章主要讲一下,会话过期后,token 刷新的一系列的事。 ❞

需求

在一个页面内,当请求失败并且返回 302 后,判断是接口过期还是登录过期,如果是接口过期,则去请求新的token,然后拿新的token去再次发起请求.

思路


  • 当初,想了一个黑科技(为了偷懒),就是拿到新的token后,直接强制刷新页面,这样一个页面内的接口就自动刷新啦~(方便是方便,用户体验却不好)

  • 目前,想到了重新请求接口时,可以配合订阅发布模式来提高用户体验

响应拦截

首先我们发起一个请求 axios({url:'/test',data:xxx}).then(res=>{})

拦截到302后,我们进入到刷新token逻辑

响应拦截代码

axios.interceptors.response.use(
   function (response) {
       if (response.status == 200) {
           return response;
      }
  },
  (err) => {
       //刷新token
       let res = err.response || {};
       if (res.data.meta?.statusCode == 302) {
           return refeshToken(res);
      } else {  
           return err;
      }
  }
);

我们后台的数据格式是根据statusCode来判断过期(你们可以根据自己的实际情况判断),接着进入refrshToken方法~

刷新token方法

//避免其他接口同时请求(只请求一次token接口)
let isRefreshToken = false;
const refeshToken = (response) => {
  if (!isRefreshToken) {
           isRefreshToken = true;
           axios({
               //获取新token接口
               url: `/api/refreshToken`,
          })
              .then((res) => {
                   const { data = '', meta = {} } = res.data;
                   if (meta.statusCode === 200) {
                       isRefreshToken = false;
                       //发布 消息
                       retryOldRequest.trigger(data);
                  } else {
                       history.push('/user/login');
                  }
              })
              .catch((err) => {
                   history.push('/user/login');
              });
      }
       //收集订阅者 并把成功后的数据返回原接口
       return retryOldRequest.listen(response);
};

看到这,有的小伙伴就有点奇怪retryOldRequest这个又是什么?没错,这就是我们男二 订阅发布模式队列。

订阅发布模式


大家如果还不了解订阅发布模式,可以点击看一下,里面有大神写的通俗易懂的例子(觉得学到的话,可以顺便帮点赞哦~)。

把失败的接口当订阅者,成功拿到新的token后再发布(重新请求接口)。

以下便是订阅发布模式代码

const retryOldRequest = {
   //维护失败请求的response
   requestQuery: [],

   //添加订阅者
   listen(response) {
       return new Promise((resolve) => {
           this.requestQuery.push((newToken) => {
               let config = response.config || {};
               //Authorization是传给后台的身份令牌
               config.headers['Authorization'] = newToken;
               resolve(axios(config));
          });
      });
  },

   //发布消息
   trigger(newToken) {
       this.requestQuery.forEach((fn) => {
           fn(newToken);
      });
       this.requestQuery = [];
  },
};

大家可以先不用关注订阅者的逻辑,只需要知道订阅者是每次请求失败后的接口(reponse)就好了。

每次进入refeshToken方法,我们失败的接口都会触发retryOldRequest.listen去订阅,而我们的requestQuery则是保存这些订阅者的队列。

注意:我们订阅者队列requestQuery是保存待发布的方法。而在成功获取新token后,retryOldRequest.trigger就会去发布这些消息(新token)给订阅者(触发订阅队列的方法)。

而订阅者(response)里面有config配置,我们拿到新的token后(发布后),修改config里面的请求头Autorzation.而借助Promise我们可以更好的拿到新token请求回来的接口数据,一旦请求到数据,我们可以原封不动的返回给原来的接口/test了(因为我们在响应拦截那里返回的是refreshToken,而refreshToken又返回的是订阅者retryOldRequest.listen返回的数据,而Listiner又返回Promise的数据,Promise又在成功请求后resolve出去)。

看到这,小伙伴们是不是觉得有点绕了~

而在真实开发中,我们的逻辑还含有登录过期(与请求过期区分开来)。我们是根据当前时间 - 过去时间 < expiresTime(epiresTime:登录后返回的有效时间)来判断是请求过期还是登录过期的。 以下是完整逻辑
以下是完整代码

const retryOldRequest = {
   //维护失败请求的response
   requestQuery: [],

   //添加订阅者
   listen(response) {
       return new Promise((resolve) => {
           this.requestQuery.push((newToken) => {
               let config = response.config || {};
               config.headers['Authorization'] = newToken;
               resolve(axios(config));
          });
      });
  },

   //发布消息
   trigger(newToken) {
       this.requestQuery.forEach((fn) => {
           fn(newToken);
      });
       this.requestQuery = [];
  },
};
/**
* sessionExpiredTips
* 会话过期:
* 刷新token失败,得重新登录
* 用户未授权,页面跳转到登录页面
* 接口过期 => 刷新token
* 登录过期 => 重新登录
* expiresTime => 在本业务中返回18000ms == 5h
* ****/

//避免其他接口同时请求
let isRefreshToken = false;
let timer = null;
const refeshToken = (response) => {
   //登录后拿到的有效期
   let userExpir = localStorage.getItem('expiresTime');
   //当前时间
   let nowTime = Math.floor(new Date().getTime() / 1000);
   //最后请求的时间
   let lastResTime = localStorage.getItem('lastResponseTime') || nowTime;
   let token = localStorage.getItem('token');

   if (token && nowTime - lastResTime < userExpir) {
       if (!isRefreshToken) {
           isRefreshToken = true;
           axios({
               url: `/api/refreshToken`,
          })
              .then((res) => {
                   const { data = '', meta = {} } = res.data;
                   isRefreshToken = false;
                   if (meta.statusCode === 200) {
                       localStorage.getItem('token', data);
                       localStorage.getItem('lastResponseTime', Math.floor(new Date().getTime() / 1000)
                      );
                       //发布 消息
                       retryOldRequest.trigger(data);
                  } else {
                      //去登录
                  }
              })
              .catch((err) => {
                   isRefreshToken = false;
                  //去登录
              });
      }
       //收集订阅者 并把成功后的数据返回原接口
       return retryOldRequest.listen(response);
  } else {
       //节流:避免重复运行
      //去登录
  }
};

// http response 响应拦截
axios.interceptors.response.use(
   function (response) {
       if (response.status == 200) {
           //记录最后操作时间
          localStorage.getItem('lastResponseTime', Math.floor(new Date().getTime() / 1000));
           return response;
      }
  },
  (err) => {
       let res = err.response || {};
       if (res.data.meta?.statusCode == 302) {
           return refeshToken(res);
      } else {
           //302 报的错误;
           return err;
      }
  }
);

以上便是我们这边的业务,如果写的不好请大佬多担待~~

如果有好方案的小伙伴也可以在评论区内互相讨论~


作者:用户3797421129853
来源:https://juejin.cn/post/7037787299202990093

收起阅读 »

通过 Performance 证明,网页的渲染是一个宏任务

别着急反驳,后面我会给出证据。调试是通过工具获取运行过程中的某一时刻或某一段时间的各方面的数据,帮助开发者理清逻辑、分析性能、排查问题等。 JS 的各种运行环境都会提供调试器,除此以外我们也会自己做一些埋点上报来做调试和统计。但是性能分析的调试工具却不能这样做...
继续阅读 »

网页的渲染是一个宏任务。 这是我下的一个结论。

别着急反驳,后面我会给出证据。

我们先来聊下什么是调试:

调试是通过工具获取运行过程中的某一时刻或某一段时间的各方面的数据,帮助开发者理清逻辑、分析性能、排查问题等。 JS 的各种运行环境都会提供调试器,除此以外我们也会自己做一些埋点上报来做调试和统计。

我们最常用的调试工具是 JS Debugger,它支持断点,可以在某处断住,查看当前上下文的变量、调用栈等,这对于理清逻辑很有帮助。

但是性能分析的调试工具却不能这样做,不能用断住的方式实时查看,因为会影响数据的真实性。所以这类工具都是通过录制一段时间的数据,然后作事后的统计和分析的方式,常用的是 Chrome Devtools 里的 Performance 工具。(甚至为了避免浏览器插件的影响,还要用无痕模式来运行网页)点击录制按钮 record 开始录制(如果想录制从页面加载开始的数据,就点击 reload 按钮),Performance 会记录下录制时间内各方面的数据。

有哪些数据呢?

网页的运行是有多个线程的,主线程负责通过 Event Loop 的方式来不断的执行 JS 和渲染,也有一些别的线程,比如合成渲染图层的线程,Web Worker 的线程等,渲染的每一帧会绘制到界面上。

网页是这样运行的,那记录的自然也都是这些数据:Performance 会记录网页的每个线程的数据,其中最重要的是主线程,也就是图中的 Main,这部分记录着 Event Loop 的执行过程,记录着 JS 执行的调用栈和页面渲染的流程。看到图中标出的一个个小灰块了么,那就是一个个 Task,也就是宏任务。Event Loop 就是循环执行宏任务。每个 Task 都有自己的调用栈,可以看到函数的执行路径,耗时等信息。图中宽度代表了耗时,可以直观的通过块的宽窄来分析性能。

执行完宏任务会执行所有的微任务,在图中也可以清晰的看到:点击每一个块可以看到代码的位置,可以定位到对应代码,这样就可以分析出哪块代码性能不好。这些是 Main 线程的执行逻辑,也就是通过 Event Loop 来不断执行 JS 和渲染。

当然,还有其他线程,比如光栅化线程,也就是负责把渲染出的图层合并成一帧的线程:总之,就像 Debugger 面前,JS 的执行过程没有秘密一样,在 Performance 面前,网页各线程的执行过程也没有秘密。

说了这么多,就是为了讲清楚调试工具和 Performance 都是干啥的,它记录了哪些信息。

我们想知道渲染是不是一个宏任务,自然可以通过 Performance 来轻易的分析出来。

我们继续看 Main 线程的 Event Loop 执行过程:你会看到一个个很小的灰块,也就是一个个 Task,每隔一段时间都会执行,点击它,就会看到其实他做的就是渲染,包括计算布局,更新渲染树,合并图层、渲染等。

这说明了什么,不就说明了渲染是一个宏任务么。

所以,我们得到了结论:渲染是一个宏任务,通过 Event Loop 来做一帧帧的渲染。

通过 Performance 调试工具,我们可以看到 Main 线程 Event Loop 的细节,看到 JS 执行和渲染的详细过程。

有时你可能会看到有的 Task 部分被标红了,还警告说这是 Long Task。因为渲染和 JS 执行都是在同一个 Event Loop 内做的,那如果有执行时间过长的 Task,自然会导致渲染被延后,也就是掉帧,用户感受到的就是页面的卡顿。

避免 Long Task,这是网页性能优化的一个重点。这也是为什么 React 使用了 Fiber 架构的可打断的组件树渲染,替代掉了之前的递归渲染整个组件树的方式,就是为了不产生 Long Task。

总结

本文目的为了证明渲染是不是一个宏任务,但其实更重要的是想讲清楚调试工具的意义。

调试工具可以分析程序运行过程中某一刻或某一段时间的各方面的数据,有两种方式:一种是 Debugger 那种断点的方式,可以看到上下文变量的值、调用栈,可以帮助理清逻辑、定位问题。而性能分析工具则是另一种方式,通过录制一段时间内的各种数据,做事后的分析和统计,这样能保证数据的真实性。

网页的性能分析工具 Performance 可以记录网页执行过程中的各个线程的执行情况,主要是主线程的 Event Loop 的执行过程,包括 JS 执行、渲染等。

通过 Performance,我们可以轻易的得出“渲染是一个宏任务”的结论。

就像在 Debugger 面前,JS 执行过程没有秘密一样。在 Performance 面前,网页的执行过程也同样没有秘密。

作者:zxg_神说要有光
来源:https://juejin.cn/post/7037839989018722340

收起阅读 »

短信跳小程序

方案:使用微信提供的url link方法 生成短链接 发送给用户 用户点击短链接会跳转到微信提供默认的默认页面 进而打开小程序场景假设:经理人发布一条运输任务 司机收到短信点击打开小程序接单经理人发布时点击发布按钮 h5调用服务端接口 传参服务端要跳转的小程序...
继续阅读 »

方案:使用微信提供的url link方法 生成短链接 发送给用户 用户点击短链接会跳转到微信提供默认的默认页面 进而打开小程序
场景假设:经理人发布一条运输任务 司机收到短信点击打开小程序接单

实现:

  • 经理人发布时点击发布按钮 h5调用服务端接口 传参服务端要跳转的小程序页面 所需要参数

  • 服务端拿access_token和前端传的参数 加链接失效时间 调用微信api

api.weixin.qq.com/wxa/generat… 得到链接 如wxaurl.cn/ow7ctZP4n8v 将此链接发送短信给司机

  • 司机点击此链接 效果如下图所示:打开小程序 h5写逻辑跳转指定页面

自己调postman调微信api post方式 接口: api.weixin.qq.com/wxa/generat…

传参

{ 
    "path": "pages/index/index",\
    "query": "?fromType=4&transportBulkLineId=111&isLinkUrlCome=1&SCANFROMTYPE=143&lineAssignRelId=111",\
     "env_version": "trial",\
     "is_expire": true,\
    "expire_time": "1638855772"\
}

返参

{
     "errcode": 0,
    "errmsg": "ok",
    "url_link": "https://wxaurl.cn/GAxGcil2Bbp"
}

url link说明文档: developers.weixin.qq.com/miniprogram…
url link方法需要服务端调用 调用接口方式参考:
developers.weixin.qq.com/miniprogram…

作者:懿小诺
来源:https://juejin.cn/post/7037356611031007239

收起阅读 »

技术选型,Vue和React的对比

1. MVVM和MVCVue是MVVM,React是MVC。MVVM(Model-View-ViewModel)是在MVC(Model View Controller)的基础上,VM抽离Controller中展示的业务逻辑,而不是替代Controller,其它...
继续阅读 »

1. MVVM和MVC

Vue是MVVM,React是MVC。

MVVM(Model-View-ViewModel)是在MVC(Model View Controller)的基础上,VM抽离Controller中展示的业务逻辑,而不是替代Controller,其它视图操作业务等还是应该放在Controller中实现。

也就是说MVVM实现的是业务逻辑组件的重用,使开发更高效,结构更清晰,增加代码的复用性。

可以理解为MVVM是MVC的升级版。

虽然React不算一个完整的MVC框架,可以认为是MVC中的V(View),但是Vue的MVVM还是更面向未来一些。

2. 数据绑定

vue是双向绑定,react是单向绑定。

单向绑定的优点是相应的可以带来单向数据流,这样做的好处是所有状态变化都可以被记录、跟踪,状态变化通过手动调用通知,源头易追溯,没有“暗箱操作”。同时组件数据只有唯一的入口和出口,使得程序更直观更容易理解,有利于项目的可维护性。

但是Vue虽然是双向绑定,但是也是单向数据流,它的双向绑定只是一个语法糖,想看正经的双向绑定可以去看下Dva。

单向绑定的缺点则是代码量会相应的上升,数据的流转过程变长,从而出现很多类似的重复代码。同时由于对应用状态独立管理的严格要求(单一的全局store),在处理局部状态较多的场景时(如用户输入交互较多的“富表单型”应用),会显得冗余。

双向绑定可以在表单交互较多的场景下,会简化大量业务无关的代码。

我认为Vue的设计方案好一些,全局性数据流使用单向,局部性数据流使用双向。

3. 数据更新

3.1 React 更新流程

React 推崇 Immutable(不可变),通过重新render去发现和更新自身。

3.2 Vue 更新流程

Vue通过收集数据依赖去发现更新。

Vue很吸引人的就是它的响应式更新,Vue首次渲染触发data的getter,从而触发依赖收集,为对应的数据创建watcher,当数据发生更改的时候,setter被触发,然后通知各个watcher在下个tick的时候更新数据。

所以说,如果data中某些数据没有在模板template 中使用的话,更新这些数据的时候,是不会触发更新的。这样的设计非常好,没有在模版上用到的变量,当它的值发生变化时,不更新视图,相当于内置了React的shouldComponentUpdate。

3.3 更新比较

  • 获取数据更新的手段和更新的粒度不一样

Vue通过依赖收集,当数据更新时 ,Vue明确知道是哪些数据更新了,每个组件都有自己的渲渲染watcher,掌管当前组件的视图更新,所以可以精确地更新对应的组件,所以更新的粒度是组件级别的。

React会递归地把所有的子组件重新render一下,不管是不是更新的数据,此时,都是新的。然后通过 diff 算法 来决定更新哪部分的视图。所以,React 的更新粒度是一个整体。

  • 对更新数据是否需要渲染页面的处理不一样

  • 只有依赖收集的数据发生更新,Vue 才会去重新渲染页面

  • 只要数据有更新(setState,useState 等手段触发更新),都会去重新渲染页面 (可以使用shouldComponentUpdate/ PureComponent 改善)

Vue的文档里有一描述说,Vue是细粒度数据响应机制,所以说数据更新这一块,我认为Vue的设计方案好一些。

4. 性能对比

借用尤大大的一段话:

模板在性能这块吊打 tsx,在 IDE 支持抹平了的前提下用 tsx 本质上是在为了开发者的偏好牺牲用户体验的性能(性能没遇到瓶颈就无所谓) 这边自己不维护框架的人吐槽吐槽我也能理解,毕竟作为使用者只需要考虑自己爽不爽。作为维护者,Vue 的已有的用户习惯、生态和历史包袱摆在那里,能激进的程度是有限的,Vue 3 的大部分设计都是戴着镣铐跳舞,需要做很多折衷。如果真要激进还不如开个新项目,或者没人用的玩票项目,想怎么设计都可以。 组件泛型的问题也有不少人提出了,这个目前确实不行,但不表示以后不会有。 最后实话实说,所有前端里面像这个问题下面的类型体操运动员们毕竟是少数,绝大部分有 intellisense + 类型校验就满足需求了。真的对类型特别特别较真的用 React 也没什么不好,无非就是性能差点。

为什么模板性能吊打TSX?

tsx和vue template其实都是一样的模版语言,tsx最终也会被编译成createElement,模板被编译成render函数,所以本质上两者都有compile-time和runtime,但tsx的特殊性在于它本身是在ts语义下的,过于灵活导致优化无从下手。但是vue的模板得益于自身本来就是DSL,有自己的文法和语义,所以vue在模板的compile-time做了巨多的优化,比如提升不变的vnode,以及blocktree配合patchflag靶向更新,这些优化在最终的runtime上会把性能拉开不少。

DSL: 一种为特定领域设计的,具有受限表达性编程语言。

所以说Vue的性能是优于React的。

5. React Hooks和Vue Hooks

其实 React Hook 的限制非常多,比如官方文档中就专门有一个章节介绍它的限制:

  1. 不要在循环,条件或嵌套函数中调用 Hook

  2. 确保总是在你的 React 函数的最顶层调用他们。

  3. 遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。

而 Vue 带来的不同在于:

  1. 与 React Hooks 相同级别的逻辑组合功能,但有一些重要的区别。 与 React Hook 不同,setup函数仅被调用一次,这在性能上比较占优。

  2. 对调用顺序没什么要求,每次渲染中不会反复调用 Hook 函数,产生的的 GC 压力较小。

  3. 不必考虑几乎总是需要 useCallback 的问题,以防止传递函数prop给子组件的引用变化,导致无必要的重新渲染。

  4. React Hook 有臭名昭著的闭包陷阱问题,如果用户忘记传递正确的依赖项数组,useEffect 和 useMemo 可能会捕获过时的变量,这不受此问题的影响。 Vue 的自动依赖关系跟踪确保观察者和计算值始终正确无误。

  5. 不得不提一句,React Hook 里的「依赖」是需要你去手动声明的,而且官方提供了一个 eslint 插件,这个插件虽然大部分时候挺有用的,但是有时候也特别烦人,需要你手动加一行丑陋的注释去关闭它。

我们认可 React Hooks 的创造力,这也是 Vue-Composition-Api 的主要灵感来源。上面提到的问题确实存在于 React Hook 的设计中,我们注意到 Vue 的响应式模型恰好完美的解决了这些问题。

--- 来自ssh

Vue的组合式API刚出来的时候确实一看好像React Hooks,我也对它的.value进行了吐槽,

但是总体来说还是更偏向于Vue Hooks。

6. 写法

React的思路是all in js,通过js来生成html,所以设计了jsx,还有通过js来操作css,社区的styled-component、jss等,所以说React的写法感觉相对自由一些,逻辑正确老子想怎么写怎么写,对于我来说,我确实更偏向于React的写法。

Vue则是把html,css,js组合到一起,就像 Web 开发多年的传统开发方式一样, vue-loader会解析文件,提取每个语言块用各自的处理方式,vue有单文件组件(SFC),可以把html、css、js写到一个文件中,html提供了模板引擎来处理。Vue感觉是给你搭了一个框架,告诉你什么地方该写什么东西,你只要按照他的要求向里面填内容就可以了,没有React那么自由,但是上手难度简单了许多。而且因为SFC,一个组件的代码会看起来很长,维护起来很头痛。

7. 理念及设计

Vue 和 React 的核心差异,以及核心差异对后续设计产生的“不可逆”影响。

Vue 和 React 在 API 设计风格和哲学理念(甚至作者个人魅力)上的不同。

Vue 和 React 在工程化预编译构建阶段,AOT 和 JIT 优化的本质差异和设计。

这个层次的比较确实对我难度确实大,我也懒得去copy,下面是Lucas大佬的分析,可以去看一下,时空隧道

作者:黑色的枫
来源:https://juejin.cn/post/7037365650251055134

收起阅读 »

微前端-从了解到动手搭建

前言微前端是 2016 年thoughtWorks提出的概念,它将微服务的理念应用于浏览器端,即将前端应用由单体应用转变成多个小型前端应用聚合的应用。各个小型前端应用可以独立运行、独立开发、独立部署。与微服务出现的原因相似,随着前端业务越来越复杂,前端的代码和...
继续阅读 »

前言

微前端是 2016 年thoughtWorks提出的概念,它将微服务的理念应用于浏览器端,即将前端应用由单体应用转变成多个小型前端应用聚合的应用。各个小型前端应用可以独立运行、独立开发、独立部署

为什么出现?

与微服务出现的原因相似,随着前端业务越来越复杂,前端的代码和业务逻辑也愈发难以维护,尤其对于中后台系统,很容易出现巨石应用,微前端由此应运而生,其根本目的就是解决巨石应用的项目复杂,系统庞大,开发人员众多,难以维护的问题。

微前端 vs 巨石应用


微前端巨石应用
可维护性拆分为框架应用、微应用、微模块后,每个业务页面都对应一个单独的仓库,应用风险性降低。所有页面都在一个仓库,经常会出现动一处则动全身,随着系统增大维护成本会逐渐升高。
开发效率结合发布、回滚、团队协作三个方面来看,单个仓库只关心一个业务页面,可以更方便快速迭代。团队多人协作时,发布排队;回滚有可能会把其他人发布的代码同时回滚掉;多分支开发时发布前沟通增加成本。
代码复用所有页面都分开维护,使用公用代码成本较大,不过共用代码抽离为npm包使用可以减小成本。一个仓库中很容易抽离公用的部分,但是要注意动一处就会动全身的结果。

架构方案

基座模式是当前比较常见的微前端架构设计。

首先以容器应用作为整个项目的主应用,负责子应用的注册,聚合,提供子运行环境、管理生命周期等。子应用就是各个独立部署、独立开发的单元。

应用注册表拥有每个应用及对应的入口。在前端领域里,入口的直接表现形式可以是路由,又或者对应的应用映射。

目前可以实现微前端架构的方案有如下:

HTTP后端路由转发(nginx)

  • ✅ 简单高效快速,同时不需要前端做额外的工作。

  • ❌ 体验并不好,相当于mpa页面,路由到每个应用需要重新刷新

iframe

  • ✅ 前端最简单的应用方式,直接嵌入,门槛最低,改动最小

  • ❌ iframe都会遇到的一些典型问题:UI 不同步,DOM 结构不共享(比如iframe中的弹框),跨域通信等

各个业务独立打到npm包中

  • ✅ 门槛低,易上手

  • ❌ 模块修改后需要重新部署发布,太麻烦。

组合式应用路由分发(基座模式)

  • ✅ 纯前端改造,体验良好,各个业务相互独立

  • ❌ 需要设计和开发,有一定成本,同时需要兼顾子页面和基座的变量污染,样式互相影响等问题

web component

  • ✅ 是一项标准,目前它包含三项主要技术,它们可以一起使用来创建封装功能的定制元素,可以在你喜欢的任何地方重用,不必担心代码冲突。应该是微前端的最终态

  • ❌ 比较新,兼容性较差

微前端页面形态

微前端基座框架需要解决的问题

路由分发

作为微前端的基座应用,是整个应用的入口,负责承载当前子应用的展示和对其他路由子应用的转发,对于当前子应用的展示,一般是由以下几步构成:

  1. 远程拉取子应用内容

  2. 将子应用的 js 和 css 抽离,采用eval来运行 js,并将 css 和 html 内容append到基座应用中留给子应用的展示区域

  3. 当子应用切换走时,同步卸载这些内容

对于路由分发而言,以采用react-router开发的基座SPA应用来举例,主要是下面这个流程:

  1. 当浏览器的路径变化后,react-router会监听hashchange或者popstate事件,从而获取到路由切换的时机。

  2. 最先接收到这个变化的是基座的router,通过查询注册信息可以获取到转发到那个子应用,经过一些逻辑处理后,采用修改hash方法或者pushState方法来路由信息推送给子应用的路由,子应用可以是手动监听hashchange或者popstate事件接收,或者采用react-router接管路由,后面的逻辑就由子应用自己控制。

应用隔离

应用隔离问题主要分为主应用和子应用,子应用和子应用之间的JavaScript执行环境隔离,CSS样式隔离,

CSS

  • 当主应用和子应用同屏渲染时,就可能会有一些样式会相互污染,如果要彻底隔离CSS污染,可以采用CSS Module 或者命名空间的方式,给每个子应用模块以特定前缀,即可保证不会互相干扰,可以采用webpack的postcss插件,在打包时添加特定的前缀。

  • 而对于子应用与子应用之间的CSS隔离就非常简单,在每次应用加载时,将该应用所有的link和style 内容进行标记。在应用卸载后,同步卸载页面上对应的link和style即可。

JavaScript隔离

  • 每当子应用的JavaScript被加载并运行时,它的核心实际上是对全局对象Window的修改以及一些全局事件的改变,例如jQuery这个js运行后,会在Window上挂载一个window.$对象,对于其他库React,Vue也不例外。为此,需要在加载和卸载每个子应用的同时,尽可能消除这种冲突和影响,最普遍的做法是采用沙箱机制(SandBox)。

  • 沙箱机制的核心是让局部的JavaScript运行时,对外部对象的访问和修改处在可控的范围内,即无论内部怎么运行,都不会影响外部的对象,需要结合 with 关键字和window.Proxy对象来实现浏览器端的沙箱。

消息通信

应用间通信有很多种方式,当然,要让多个分离的子应用之间要做到通信,本质上仍离不开中间媒介或者说全局对象。所以对于消息订阅(pub/sub)模式的通信机制是非常适用的,在基座应用中会定义事件中心Event,每个子应用分别来注册事件,当被触发事件时再有事件中心统一分发,这就构成了基本的通信机制。

当然,如果基座和子应用采用的是React或者是Vue,是可以结合Redux和Vuex来一起使用,实现应用之间的通信。

搭一个看看?

qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。也是支付宝内部广泛使用的微前端框架。

那么我们就使用 qiankun 从头搭一个demo出来体验一下

基座

  • 基座我们使用react,自行使用 create-react-app 创建一个react项目即可。

  • npm install qiankun -s

  • 在基座中需要调用 registerMicroApps 注册子应用,然后调用start启动

因此在 index.js 中插入如下代码

import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
{
   name: 'vueApp',
   entry: '//localhost:8080',
   container: '#container',
   activeRule: '/app-vue',
},
]);

// 启动 qiankun
start();
  • 修改App.js

    • 加入一些 antd 元素,让demo像样一些

    • 同时,由于qiankun根据路由来加载不同微应用,我们也安装 react-router-dom

    • npm install react-router-dom

    • 安装完之后修改 App.js 如下:

import { useState } from 'react';
import { Layout, Menu } from 'antd';
import { PieChartOutlined } from '@ant-design/icons';
import { Link } from 'react-router-dom'
import './App.css';

const { Header, Content, Footer, Sider } = Layout;

const App = () => {
 const [collapsed, setCollapsed] = useState(false);
 
 const onCollapse = collapsed => {
   setCollapsed(collapsed);
};

 return (
   <Layout style={{ minHeight: '100vh' }}>
     <Sider collapsible collapsed={collapsed} onCollapse={onCollapse}>
       <div className="logo" />
       <Menu theme="dark" defaultSelectedKeys={['1']} mode="inline">
         <Menu.Item key="1" icon={<PieChartOutlined />}>
           <Link to="/app-vue">Vue应用</Link>
         </Menu.Item>
       </Menu>
     </Sider>
     <Layout className="site-layout">
       <Header className="site-layout-background" style={{ padding: 0 }} />
       <Content style={{ margin: '16px' }}>
         <div id="container" className="site-layout-background" style={{ minHeight: 360 }}></div>
       </Content>
       <Footer style={{ textAlign: 'center' }}>This Project ©2021 Created by DiDi</Footer>
     </Layout>
   </Layout>
);
}

export default App;
  • 记得修改 index.js,把 App 组件用 react-router-dom 的 BrowserRouter 包一层,让 BrowserRouter 作为顶层组件才可以跳转

  • 至此,基座搭好了

子页面

尝试使用vue作为子页面,来体现微前端的技术隔离性。

  • 使用vue-cli创建vue2.x项目

  • 修改main.js如下:

import Vue from "vue/dist/vue.js";
import App from "./App.vue";
import router from "./router";

Vue.config.productionTip = false;

// window.__POWERED_BY_QIANKUN__ 为true 说明在 qiankun 架构中
// 修改webpack的publicPath,将子应用资源加载的公共基础路径设为 qiankun 包装后的路径
// 这个 __INJECTED_PUBLIC_PATH_BY_QIANKUN__ 的实际地址是子应用的服务器地址,子应用的应用资源都在他本身的实际服务器上
if (window.__POWERED_BY_QIANKUN__) {
 // eslint-disable-next-line no-undef
 __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

let instance = null;
function render(props = {}) {
 const { container } = props;
 instance = new Vue({
   router,
   render: (h) => h(App),
}).$mount(container ? container.querySelector("#app") : "#app");
}

// 独立运行时 直接渲染
if (!window.__POWERED_BY_QIANKUN__) {
 render();
}

// 应用需要导出 bootstrap、mount、unmount 三个生命周期钩子,以供主应用在适当的时机调用。
export async function bootstrap() {
 console.log("[vue] vue app bootstraped");
}

export async function mount(props) {
 console.log("[vue] props from main framework", props);
 render(props);
}

export async function unmount() {
 instance.$destroy();
 instance.$el.innerHTML = "";
 instance = null;
}
  • router.js配置如下:

import Vue from "vue/dist/vue.js";
import VueRouter from "vue-router";

Vue.use(VueRouter);

const routes = [
{
   path: "/test",
   name: "Test",
   component: () => import("./components/Test.vue"),
},
{
   path: "/hello",
   name: "Hello",
   component: () => import("./components/Hello.vue"),
},
];

const router = new VueRouter({
 base: window.__POWERED_BY_QIANKUN__ ? "/app-vue/" : "/",
 mode: "history",
 routes,
});

export default router;
  • 根目录下新建vue.config.js 用来配置webpack,内容如下:

const { name } = require("./package");
module.exports = {
 devServer: {
   // 跨域
   headers: {
     "Access-Control-Allow-Origin": "*",
  },
},
 configureWebpack: {
   output: {
     library: `${name}-[name]`,
     // 把微应用打包成 umd 库格式
     libraryTarget: "umd",
     jsonpFunction: `webpackJsonp_${name}`,
  },
},
};

启动

基座和子应用分别启动,可以看到,子应用已经加载到了主应用中:


作者:visa
来源:https://juejin.cn/post/7037386845751083021

收起阅读 »

实现一个逐步递增的数字动画

背景 可视化大屏项目使用最多的组件就是数字组件,展示数据的一个变化,为了提高视觉效果,需要给数字增加一个滚动效果,实现一个数字到另一个数字逐步递增的滚动动画。 先上一个思维导图: 一、实现类似滚轮的效果,容器固定,数字向上滚动 先列举所有的可能的值形成一个...
继续阅读 »

背景


可视化大屏项目使用最多的组件就是数字组件,展示数据的一个变化,为了提高视觉效果,需要给数字增加一个滚动效果,实现一个数字到另一个数字逐步递增的滚动动画。


先上一个思维导图:


思维导图.png


一、实现类似滚轮的效果,容器固定,数字向上滚动


demo1.gif


先列举所有的可能的值形成一个纵向的列表,然后固定一个容器,匀速更改数字的偏移值。


下面来介绍一下这种方案的实现,元素值从0到9一共十个值,每个数字占纵向列表的10%,所以纵向偏移值依次为为0% —> -90%


实现:


<ul>
<li>
<span>0123456789</span>
</li>
</ul>

ul{
margin-top: 200px;
}
ul li{
margin:0 auto;
width: 20px;
height: 30px;
text-align: center;
border:2px solid rgba(221,221,221,1);
border-radius:4px;
}
ul li span{
position: absolute;
color: #fff;
top: 30%;
left: 50%;
transform: translate(-50%,0);
transition: transform 500ms ease-in-out;
writing-mode: vertical-rl;
text-orientation: upright;
letter-spacing: 17px;
}

let spanDom = document.querySelector('span')
let start = 0
setInterval(() =>{
start++
if(start>9){
start = 0
}
spanDom.style.transform = `translate(-50%,-${start*10}%)`
}, 1000)

上述代码存在一个问题,当我们从9到0的时候,容器偏移从-90%直接到了0%。 但是由于设定了固定的过渡动画时间,就会出现一个向反方向滚动的情况,为了解决这个问题,可以参考无缝滚动的思路




  • 在9后面复制一份0,




  • 当纵向列表滚动到9的时候,继续滚动到复制的0




  • 滚动到复制的0的时候,把列表的偏移位置改为0,并且控制动画时间为0




<ul>
<li>
<span>01234567890</span>
</li>
</ul>

let spanDom = document.querySelector('span')
let start = 0
var timer = setInterval(fn, 1000);
function fn() {
start++
clearInterval(timer)
timer = setInterval(fn,start >10 ? 0 : 1000);
if(start>10){
spanDom.style.transition = `none`
start = 0
}else{
spanDom.style.transition = `transform 500ms ease-in-out`
}
spanDom.style.transform = `translate(-50%,-${start/11*100}%)`
}

demo2.gif


利用两个元素实现滚动


仔细看动图的效果,事实上在在视口只有两个元素,一个值之前的值,一个为当前的值,滚动偏移值只需设置translateY(-100%)


具体思路:




  • 声明两个变量,分别存放之前的值prev,以及变化后的值cur;声明一个变量play作为这两个值的滚动动画的开关




  • 使用useEffect监听监听传入的值:如果是有效的数字,那么把没有变化前的值赋值给prev,把当前传入的值赋值给cur,并且设置palytrue开启滚动动画




下面是调整后的代码结构:


 <div className={styles.slider}>
{[prev, cur].map((item, index) => (
<span key={index} className={`${styles['slider-text']} ${playing && styles['slider-ani']} ${(prev === 0 && cur === 0 && index ===0) && styles['slider-hide']}`}>
{item}
</span>
))}
</div>

const { value} = props
const [prev, setPrev] = useState(0)
const [cur, setCur] = useState(0)
const [playing, setPlaying] = useState(false)

const play = (pre, current) => {
setPrev(pre)
setCur(current)
setPlaying(false)
setTimeout(() => {
setPlaying(true)
}, 20)
}

useEffect(() => {
if (!Number.isNaN(value)) {
play(cur, value)
} else {
setPrev(value)
setCur(value)
}
}, [value])

.slider {
display: flex;
flex-direction: column;
height: 36px;
margin-top: 24%;
overflow: hidden;
text-align: left;
}

.slider-text {
display: block;
height: 100%;
transform: translateY(0%);
}

.slider-ani {
transform: translateY(-100%);
transition: transform 1s ease;
}
.slider-hide {
opacity: 0;
}


实现多个滚轮的向上滚动的数字组件


组件.gif


利用H5的requestAnimationFrame()API实现数字逐步递增的动画效果


实现一个数字的逐渐递增的滚动动画,并且要在指定时间内完成。要看到流畅的动画效果,就需要在更新元素状态时以一定的频率进行,JS动画都是通过在很短的时间内不停的渲染/绘制元素做到的,所以计时器一直都是Javascript动画的核心技术,关键就是刷新的间隔时间,刷新时间需要尽量短,这样动画效果才能显得更加流畅,不卡顿;同时刷新间隔又不能太短,需要确保浏览器有能力渲染动画


大多数电脑显示器的刷新频率是 60Hz,即每秒重绘 60次。因此平滑动画的最佳循环间隔是通常是 1000ms/60,约等于16.6ms


计时器对比




  • 与 setTimeout 和 setInterval 不同,requestAnimationFrame 不需要程序员自己设置时间间隔。setTimeout 和 setInterval 的问题是精确度低。它们的内在运行机制决定了时间间隔参数实际上只是指定了把动画代码添加到浏览器 UI 线程队列中以等待执行的时间。如果队列前面已经加入了其他任务,那动画代码就要等前面的任务完成后再执行。




  • requestAnimationFrame 采用系统时间间隔,它会要求浏览器根据自己的频率进行一次重绘,保持最佳绘制效率,不会因为间隔时间过短,造成过度绘制,增加开销;也不会因为间隔时间太长,使用动画卡顿不流畅,让各种网页动画效果能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。




  • requestAnimationFrame 会把每一帧中的所有 DOM 操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率。




  • requestAnimationFrame 对于隐藏或不可见元素,将不会进行重绘或回流,就意味着使用更少的 CPU、GPU 和内存使用量。




  • requestAnimationFrame 是由浏览器专门为动画提供的API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了CPU开销。




requestAnimationFrame实现滚动动画思路



  • 动画开始,记录开始动画的时间 startTimeRef.current


const startTimeRef = useRef(Date.now());
const [t, setT] = useState(Date.now());


  • 之后每一帧动画,记录从开始动画经过了多长时间,计算出当前帧的所到达的数字应该是多少,即currentValue


useEffect(() => {
const rafFunc = () => {
const now = Date.now();
const t = now - startTimeRef.current;
if (t >= period) {
setT(period);
} else {
setT(t);
requestAnimationFrame(rafFunc);
}
};
let raf;
if (autoScroll) {
raf = requestAnimationFrame(rafFunc);
startTimeRef.current = Date.now();
} else {
raf && cancelAnimationFrame(raf);
}
return () => raf && cancelAnimationFrame(raf);
}, [period, autoScroll]);

const currentValue = useMemo(() => ((to - from) / period) * t + from, [t, period, from, to]);


  • 针对当前每个数字位上的数字进行比较,如果有变化,进行偏移量的变化,偏移量体现在当前数字位上的数字与下一位数字之间的差值,这个变化每一帧都串起来形成了滚动动画


成果展示


成果.gif


成果2.gif


作者:我就是胖虎
链接:https://juejin.cn/post/7025913017627836452

收起阅读 »

前端金额格式化处理

前端项目中,金额格式化展示是很常见的需求,在此整理了一些通用的处理方式,如 toLocaleString();正则匹配;slice()循环截取等等;也解决了小数点精度问题 以此为例:12341234.246 => ¥ 12,341,234.25 方式一...
继续阅读 »

前端项目中,金额格式化展示是很常见的需求,在此整理了一些通用的处理方式,如 toLocaleString();正则匹配;slice()循环截取等等;也解决了小数点精度问题



以此为例:12341234.246 => ¥ 12,341,234.25


方式一:采用浏览器自带的Number.prototype.toLocaleString()处理整数部分,小数部分直接用Number.prototype.toFixed()四舍五入处理


// v1.0
const formatMoney = (money, symbol = "", decimals = 2) => {
if (!(money && money > 0)) {
return 0.0;
}

let arr = money.toFixed(decimals).toString().split(".");
let first = parseInt(arr[0]).toLocaleString();
let result = [first, arr[1]].join(".");
return `${symbol} ${money.toFixed(decimals)}`;
};

formatMoney(12341234.246); // 12,341,234.25
formatMoney(12341234.246, "¥", 1); // ¥ 12,341,234.2

2021.11.9 更改记录 我之前写复杂了,经过评论区[黄景圣]的指点,优化如下:


// v2.0 简化函数
const formatMoney = (money, symbol = "", decimals = 2) =>
`${symbol} ${parseFloat(money.toFixed(decimals)).toLocaleString()}`;

formatMoney(12341234.246, "¥", 2) // ¥ 12,341,234.25

// 或者只用toLocaleString()处理
const format = (money, decimals = 2) =>
money.toLocaleString("zh", {
style: "currency",
currency: "CNY",
maximumFractionDigits: decimals,
useGrouping: true, // false-没有千位分隔符;true-有千位分隔符
});
format(12341234.246); // ¥12,341,234.25

2021.11.10 更改记录 经过评论区[你摸摸我这料子]的提示,解决了 toFixed() 精度失效的问题,具体可查看前端小数展示精度处理


// 测试数据如下:
formatMoney(12.035); // 12.04 正常四舍五入
formatMoney(12.045); // 12.04 异常,应该为12.05,没有四舍五入

// v3.0 解决toFixed()问题
const formatToFixed = (money, decimals = 2) => {
return (
Math.round((parseFloat(money) + Number.EPSILON) * Math.pow(10, decimals)) /
Math.pow(10, decimals)
).toFixed(decimals);
};
const formatMoney = (money, symbol = "", decimals = 2) =>
`${symbol}${parseFloat(formatToFixed(money, decimals)).toLocaleString()}`;

formatMoney(12341234.035, '¥'); // ¥12,341,234.04
formatMoney(12341234.045, '¥'); // ¥12,341,234.05

2021.11.17 更改记录 通过评论区[Ryan_zhang]的提醒,解决了保留四位小数显示的问题


// v4.0 只更改了formatMoney函数,其他的不变
const formatMoney = (money, symbol = "", decimals = 2) =>
`${symbol}${parseFloat(formatToFixed(money, decimals)).toLocaleString(
"zh",
{
maximumFractionDigits: decimals,
useGrouping: true,
}
)}`;
formatMoney(12341234.12335, "¥", 4); // ¥12,341,234.1234
formatMoney(12341234.12345, "¥", 4); // ¥12,341,234.1235

方式二:使用正则表达式处理整数部分,小数部分同上所示。有个《JS 正则迷你书》介绍正则表达式挺好的,在 2.4.2 章就讲了“数字的千位分隔符表示法”,介绍的很详细,推荐看看。



  • \b:单词边界,具体就是 \w 与 \W 之间的位置,也包括 \w 与 ^ 之间的位置,和 \w 与 $ 之间的位置

  • \B :\b 的反面的意思,非单词边界

  • (?=p):其中 p 是一个子模式,即 p 前面的位置,或者说,该位置后面的字符要匹配 p


/**
* @params {Number} money 金额
* @params {Number} decimals 保留小数点后位数
* @params {String} symbol 前置符号
*/
const formatMoney = (money, symbol = "", decimals = 2) => {
let result = money
.toFixed(decimals)
.replace(/\B(?=(\d{3})+\b)/g, ",")
.replace(/^/, `${symbol}`);
return result;
};

formatMoney(12341234.246, "$", 2); // $12,341,234.25

// v2.0 解决toFixed()问题
const formatMoneyNew = (money, symbol = "", decimals = 2) =>
formatToFixed(money, decimals)
.replace(/\B(?=(\d{3})+\b)/g, ",")
.replace(/^/, `${symbol}`);

formatMoneyNew(12341234.035, "¥", 2); // ¥12,341,234.04
formatMoneyNew(12341234.045, "¥", 2); // ¥12,341,234.05

方式三:循环字符串,通过 slice 截取实现



  • substring(start, end):包含 start,不包含 end

  • substr(start, length):包含 start,长度为 length

  • slice(start, end):可操作数组和字符串;包含 start,不包含 end

  • splice(start, length, items):只能针对数组;增删改都可以


const formatMoney = (money, symbol = "", decimals = 2) => {
// 改造前
// let arr = money.toFixed(decimals).toString().split(".");
// 改造后
let arr = formatToFixed(money, decimals).toString().split(".");
let num = arr[0];
let first = "";
su;
while (num.length > 3) {
first = "," + num.slice(-3) + first;
num = num.slice(0, num.length - 3);
}
if (num) {
first = num + first;
}
return `${symbol} ${[first, arr[1]].join(".")}`;
};

formatMoney(12341234.246, "$", 2); // $ 12,341,234.25
formatMoney(12341234.035, "¥", 2); // ¥ 12,341,234.04
formatMoney(12341234.045, "¥", 2); // ¥ 12,341,234.05

2021.11.24 更改记录 通过评论区[SpriteBoy]和[maxxx]的提醒,采用Intl内置的NumberFormat试试


方式四:Intl.NumberFormat,用法和toLocaleString()挺相似的


const formatMoney = (money, decimals = 2) => {
return new Intl.NumberFormat("zh-CN", {
style: "currency", // 货币形式
currency: "CNY", // "CNY"是人民币
currencyDisplay: "symbol", // 默认“symbol”,中文中代表“¥”符号
// useGrouping: true, // 是否使用分组分隔符,如千位分隔符或千/万/亿分隔符,默认为true
// minimumIntegerDigits: 1, // 使用的整数数字的最小数目.可能的值是从1到21,默认值是1
// minimumFractionDigits: 2, // 使用的小数位数的最小数目.可能的值是从 0 到 20
maximumFractionDigits: decimals, // 使用的小数位数的最大数目。可能的值是从 0 到 20
}).format(money);
};

console.log(formatMoney(12341234.2, 2)); // ¥12,341,234.20
console.log(formatMoney(12341234.246, 1)); // ¥12,341,234.2
console.log(formatMoney(12341234.035, 2)); // ¥12,341,234.04
console.log(formatMoney(12341234.045, 2)); // ¥12,341,234.05
console.log(formatMoney(12341234.12335, 4)); // ¥12,341,234.1234
console.log(formatMoney(12341234.12345, 4)); // ¥12,341,234.1235

作者:时光足迹
链接:https://juejin.cn/post/7028086399601475591

收起阅读 »

清空数组的几个方式

1. 前言 前两天在工作当中遇到一个问题,在vue3中使用reactive生成的响应式数组如何清空,当然我一般清空都是这么写: let array = [1,2,3]; array = []; 不过这么用在reactive代理的方式中还是有点问题,比如...
继续阅读 »

1. 前言


前两天在工作当中遇到一个问题,在vue3中使用reactive生成的响应式数组如何清空,当然我一般清空都是这么写:


  let array = [1,2,3];
array = [];

不过这么用在reactive代理的方式中还是有点问题,比如这样:


    let array = reactive([1,2,3]);
watch(()=>[...array],()=>{
console.log(array);
},)
array = reactive([]);

很显然,因为丢失了对原来响应式对象的引用,这样就直接失去了监听


2. 清空数据的几种方式


当然,作为一名十年代码经验常年摸鱼的我,立马就给出了几个解决方案。


2.1 使用ref()


使用ref,这是最简便的方法:


    const array = ref([1,2,3]);

watch(array,()=>{
console.log(array.value);
},)

array.value = [];

image.png


2.2 使用slice


slice顾名思义,就是对数组进行切片,然后返回一个新数组,感觉和go语言的切片有点类似。当然用过react的小伙伴应该经常用slice,清空一个数组只需要这样写:


    const array = ref([1,2,3]);

watch(array,()=>{
console.log(array.value);
},)

array.value = array.value.slice(0,0);

image.png
不过需要注意要使用ref


2.3 length赋值为0


个人比较喜欢这种,直接将length赋值为0


    const array = ref([1,2,3]);

watch(array,()=>{
console.log(array.value);
},{
deep:true
})

array.value.length = 0;

而且,这种只会触发一次,但是需要注意watch要开启deep:


image.png


不过,这种方式,使用reactive会更加方便,也不用开启deep:


    const array = reactive([1,2,3]);

watch(()=>[...array],()=>{
console.log(array);
})

array.length = 0;

image.png


2.4 使用splice


副作用函数splice也是一种方案,这种情况同时也可以使用reactive:


    const array = reactive([1,2,3]);

watch(()=>[...array],()=>{
console.log(array);
},)

array.splice(0,array.length)

不过要注意,watch会触发多次:


1636352459(1).jpg


当然也可以使用ref,但是注意这种情况下,需要开启deep:


    const array = ref([1,2,3]);

watch(array,()=>{
console.log(array.value);
},{
deep:true
})

array.value.splice(0,array.value.length)

image.png


但是可以看到ref也和reactive一样,会触发多次。


3. 总结


以上是我个人工作中的对于清空数组的总结,但是可以看到splice还是有点特殊的,会触发多次,不过为什么会产生这种差异还有待研究。


v2-db16a663d4445bb2044d2635ab81f2a2_720w.jpg


作者:RadiumAg
链接:https://juejin.cn/post/7028086044285206564

收起阅读 »

Android | 彻底理解 View 的坐标

Android | 彻底理解 View 的坐标前言如果你是一位从事 Android 原生开发的工程师,那么肯定会对 View 的各种坐标感到迷惑,不理解他们的真正含义。因为曾经我也和你们一样,面对他们时感到陌生和害怕。现在我将这些知识点整理成文,希望可以给大家...
继续阅读 »

Android | 彻底理解 View 的坐标

前言

如果你是一位从事 Android 原生开发的工程师,那么肯定会对 View 的各种坐标感到迷惑,不理解他们的真正含义。因为曾经我也和你们一样,面对他们时感到陌生和害怕。现在我将这些知识点整理成文,希望可以给大家一些帮助。

View 的坐标分为四大类:位置坐标,内容滚动坐标,平移坐标,触摸坐标。 通过阅读本文,读者能够在理解各种 View 坐标的基础上,今后在面对动画和触摸事件的处理会更加的游刃有余。

预备知识

如果你对以下知识有过了解,阅读本文将会很轻松。

  1. 了解 View 的属性动画;View 触摸事件的分发;View 的测量、布局过程
  2. 了解 Kotlin 基础语法

环境

文中所有的代码和运行截图,基于以下开发环境。
Android Studio 4.1.1
Kotlin 1.4.20
程序运行系统 Android 5.1

View 的位置坐标

View 的位置坐标是我们日常开发中最常见的一类坐标,分别是左、上、右、下,获取他们的值也很简单。

getLeft()
getTop()
getRight()
getBottom()

如上所示,通过 View 的上面四个方法,就可以获取到 View 的位置坐标了。需要注意以下三点。

1. (left,top) 代表 View 的左上角坐标,(right,bottom) 代表 View 的右下角坐标
2. 位置坐标是一种相对于父容器的坐标,即坐标系原点是父容器的左上角
3. 位置坐标不会因为 View 的内容滚动、View 的平移而改变,他们在 View 的测量、布局过程结束后就确定了

View 的内容滚动坐标

通过 View 的下面两个方法,可以得到 View 内容滚动后的左上角坐标。

getScrollX()
getScrollY()

需要注意 View 的内容滚动坐标是相对于 View 自身的坐标,即坐标系原点是 View 自身的左上角,并不是父容器。 如下伪代码和运行截图所示,绿色区域是一个 TextView ,当我们点击绿色区域的时候,TextView 的内容会向右滚动 100px的距离,根据运行后的截图,可以得出如下结论。

1. View 的位置坐标并不会因为 View 的内容滚动后而发生改变,这在上面已经说明过
2. 当一个 View 的内容从左向右滚动时,getScrollX() 是负值,同理当一个 View 的内容从上往下滚动时,getScrollY()也是负值。反之,从右向左,从下往上就是正值

viewBinding.tvScroll.scrollTo(-100,0)

初始坐标.png

向右滚动100px.png

View 的平移坐标

读者在实际开发中,或多或少都接触过 View 的属性动画,大概是平移、旋转、缩放三种。而平移的运用场景在 Android 中实在是太多了,基本你能看到的 View 滑动效果,都是通过属性动画的平移来实现的。通过下面两个方法,可以得到 View 左上角的平移坐标。需要注意 View 的平移坐标同样是相对于 View 自身的坐标,即坐标系原点是 View 自身的左上角,并不是父容器

getTranslationX()
getTranslationY()

当然在你需要的时候,通过 View 的下面两个方法,仍然可以获得 View 相对于父容器的平移坐标。

getX()
getY()

他们两者之间的数学关系如下

getX() = getLeft() + getTranslationX()
getY() = getTop() + getTranslationY()

如下图伪代码和运行截图所示,绿色区域仍然是一个 TextView,当我们点击绿色区域的时候,使用属性动画,让 TextView 向右平移 100px,根据运行后的截图,可以得出如下结论。

1. View 的位置坐标不会随 View的平移而改变
2. 和 View 的内容滚动不一样,View 的平移是整个 View 都向右平移
3. 向右平移 getTranslationX() 是正值,同理向下平移 getTranslationY()也是正值。反之就是负值

val translationXAnim = ObjectAnimator.ofFloat(viewBinding.tvScroll,"translationX",0f,100f).setDuration(2*1000)
translationXAnim.start()

初始坐标.png

向右平移100px.png

这里读者思考一个问题,如果让你实现一个 View 的滑动效果时,选择内容滚动还是属性动画平移? 很显然,平移相对内容滚动有诸多优点,首先是平移坐标的正负值符合人们的直观感受,其次平移是整个 View 的平移,实际应用场景更多,没有明显的缺点。

View 的触摸坐标

View 的触摸坐标和触摸事件是相关联的,获取触摸坐标有如下两组方法。

第一组,获取 View 触摸点相对于 View 自身左上角的坐标

eventX = event.getX()
eventY = event.getY()

第二组,获取 View 触摸点相对于设备屏幕左上角的坐标

rawX = event.getRawX()
rawY = event.getRawY()

注意上面伪代码中 event 的类型是 MotionEvent,读者可以通过调用 View 的 setOnTouchListener 方法,或重写 View 的 onTouchEvent 方法,来使用这个对象。

如下运行截图所示,绿色区域仍然是一个 TextView,不过这里为它设置了 50px 的左边距和 50px 的上边距。点击绿色区域的任意一处,你都会看到,rawX 和 eventX 始终相差 50px,rawY 和 eventY 始终相差 75px。 根据上面的分析 rawY 和 eventY 也应该相差 50px 才对?其实多出来的 25px 是屏幕状态栏的高度,这证实了上面的结论。

1. event.getX() 和 event.getY() 是 View 触摸点相对于 View 自身左上角的坐标
2. event.getRawX() 和 event.getRawY() 是 View 触摸点相对于设备屏幕左上角的坐标

触摸坐标.png

通过下图,读者或许能够更好理解。

触摸坐标

写在最后

本文是对 View 坐标的一次实践与总结,希望本文能够给读者一点帮助。

原文链接:https://juejin.cn/post/7037320714935861284?utm_source=gold_browser_extension

收起阅读 »

Android asm加注解实现自动Log打印

Android asm加注解实现自动Log打印前言在Android开发中有时候调试问题要给方法加很多的log,很麻烦,所以结合asm用注解的方式来自动在方法中插入log,这样方便开发时候调试。当然通过asm插入的log应该需要包含方法的参数,方法的返回值,有时...
继续阅读 »

Android asm加注解实现自动Log打印

前言

在Android开发中有时候调试问题要给方法加很多的log,很麻烦,所以结合asm用注解的方式来自动在方法中插入log,这样方便开发时候调试。当然通过asm插入的log应该需要包含方法的参数,方法的返回值,有时候也需要获取对象里面的变量值等。

hanno

_    _
| | | |
| |__| | __ _ _ __ _ __ ___
| __ |/ _` | '_ \| '_ \ / _ \
| | | | (_| | | | | | | | (_) |
|_| |_|\__,_|_| |_|_| |_|\___/
复制代码

通过字节码插件实现注解打印log,注解可以加在类上面,也可以加在方法上面,当加在类上面时会打印全部方法的log,当加在方法上面时打印当前方法的log

使用方法

1、类中全部方法打印log

@HannoLog
class MainActivity : AppCompatActivity() {
// ...
}
复制代码

只要在类上面加上@HannoLog注解就可以在编译的时候给这个类中所有的方法插入log,运行时输出log。

2、给类中的某些方法加log

class MainActivity : AppCompatActivity() {
@HannoLog(level = Log.INFO, enableTime = false,watchField=true)
private fun test(a: Int = 3, b: String = "good"): Int {
return a + 1
}
}
复制代码

通过在方法上面添加注解可以在当前方法中插入log。 3、打印的log

//D/MainActivity: ┌───────────────────────────────────------───────────────────────────────────------
//D/MainActivity: │ method: onCreate(android.os.Bundle)
//D/MainActivity: │ params: [{name='savedInstanceState', value=null}]
//D/MainActivity: │ time: 22ms
//D/MainActivity: │ fields: {name='a', value=3}{name='b', value=false}{name='c', value=ccc}
//D/MainActivity: │ thread: main
//D/MainActivity: └───────────────────────────────────------───────────────────────────────────------
复制代码

其中method是当前方法名,params是方法的参数名和值,time方法的执行时间,fields是当前对象的fields值,thread当前方法执行的线程。

HannoLog参数解释

可以通过level来设置log的级别,level的设置可以调用Log里面的INFO,DEBUG,ERROR等。enableTime用来设置是否打印方法执行的时间,默认是false,如果要打印设置enableTime=true. tagName用于设置log的名称,默认是当前类名,也可以通过这个方法进行设置。

1、level控制log打印的等级,默认是log.d,可以通过@HannoLog(level = Log.INFO)来设置等级,支持Log.DEBUG,Log.ERROR等。

2、enableTime控制是否输出方法的执行时间,默认是false,如果要打印可以通过@HannoLog(enableTime=true)来设置。

3、tagName设置tag的名称,默认是当前类名,也可以通过 @HannoLog(tagName = "test")来设置。

4、watchField用于观察对象中的field值,通过@HannoLog(watchField = true)设置,由于静态方法中不能调用非静态的field所以这个参数在静态方法上统一不生效。

重要的类

1、HannoLog HannoLog是注解类,里面提供了控制参数。对应上面的HannoLog参数解释

/**
*
*
*
* create by 胡汉君
* date 2021/11/10 17:38
* 定义一个注解,用于标注当前方法需要打印log
*/

@Retention(RetentionPolicy.CLASS)
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE})
public @interface HannoLog {
//定义一下log的级别,默认是3,debug级别
int level() default Log.DEBUG;
/**
* @return 打印方法的运行时间
*/

boolean enableTime() default false;

/**
* @return tag的名称,默认是类名,也可以设置
*/

String tagName() default "";

/**
* @return 是否观察field的值,如果观察就会就拿到对象里面全部的field值
*/

boolean watchField() default false;
}
复制代码

2、HannoExtension

public class HannoExtension {
//控制是否使用Hanno
boolean enable;
//控制是否打印log
boolean openLog = true;

public boolean isEnableModule() {
return enableModule;
}

public void setEnableModule(boolean enableModule) {
this.enableModule = enableModule;
}

//设置这个值为true可以给整个module的方法增加log
boolean enableModule = false;

public boolean isEnable() {
return enable;
}

public boolean isOpenLog() {
return openLog;
}

public void setOpenLog(boolean openLog) {
this.openLog = openLog;
}

public void setEnable(boolean enable) {
this.enable = enable;
}
}
复制代码

HannoExtension提供gradle.build文件是否开启plugin 和打印执行plugin的log 默认情况下添加HannoLog之后会进行asm插装,也可以通过在module的build.gradle文件中添加以下配置使在编译时不执行字节码插装提高编译速度

apply plugin: 'com.hanking.hanno'
hannoExtension{
enable=false
openLog=false
}
复制代码

实现原理

hanno是通过asm字节码插桩方式来实现的。Android项目的编译过程如下图: 在这里插入图片描述 java编译器会将.java类编译生成.class类,asm可以用来修改.class类,通过对.class类的修改就可以达到往已有的类中加入代码的目的。一个.java文件经过Java编译器(javac)编译之后会生成一个.class文件。 在.class文件中,存储的是字节码(ByteCode)数据,如下图所示。 在这里插入图片描述 ASM所的操作对象是是字节码(ByteCode)的类库。ASM处理字节码(ByteCode)数据的流程是这样的:

第一步,将.class文件拆分成多个部分;

第二步,对某一个部分的信息进行修改;

第三步,将多个部分重新组织成一个新的.class文件。

ClassFile

.class文件中,存储的是ByteCode数据。但是,这些ByteCode数据并不是杂乱无章的,而是遵循一定的数据结构。

ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
复制代码

字节码的类库和ClassFile之间关系 在这里插入图片描述

asm的组成

从组成结构上来说,ASM分成两部分,一部分为Core API,另一部分为Tree API。

  • 其中,Core API包括asm.jar、asm-util.jar和asm-commons.jar;
  • 其中,Tree API包括asm-tree.jar和asm-analysis.jar。

在这里插入图片描述

asm中重要的类

  • ClassReader类,负责读取.class文件里的内容,然后拆分成各个不同的部分。
  • ClassVisitor类,负责对.class文件中某一部分里的信息进行修改。
  • ClassWriter类,负责将各个不同的部分重新组合成一个完整的.class文件。

在这里插入图片描述

.class文件 --> ClassReader --> byte[] --> 经过各种转换 --> ClassWriter --> byte[] --> .class文件
复制代码

ClassVisitor类

ClassVisitor是一个抽象类,实现类有ClassWriter类(Core API)和ClassNode类(Tree API)。

public abstract class ClassVisitor {
protected final int api;
protected ClassVisitor cv;
}
复制代码
  • api字段:int类型的数据,指出了当前使用的ASM API版本。
  • cv字段:ClassVisitor类型的数据,它的作用是将多个ClassVisitor串连起来

在这里插入图片描述

classVisitor的方法

visit()、visitField()、visitMethod()和visitEnd()。

visitXxx()方法与ClassFile ClassVisitor的visitXxx()方法与ClassFile之间存在对应关系。在ClassVisitor中定义的visitXxx()方法,并不是凭空产生的,这些方法存在的目的就是为了生成一个合法的.class文件,而这个.class文件要符合ClassFile的结构,所以这些visitXxx()方法与ClassFile的结构密切相关。 1、visit()方法 用于生成类或者接口的定义,如下生成一个为printField的类,因为如果类默认继承的父类是Object类,所以superName是” java/lang/Object “。

cw.visit(52, ACC_PUBLIC + ACC_SUPER, "com/hank/test/PrintField", null, "java/lang/Object", null);
复制代码

2、visitField()方法 对应classFile中的field_info,用于生成对象里面的属性值。通过visitField生成一个属性,如下:

FieldVisitor fv;
{
fv = cw.visitField(ACC_PRIVATE + ACC_FINAL + ACC_STATIC, "a", "I", null, new Integer(2));
fv.visitEnd();
}
复制代码

3、visitMethod()方法 用于生成一个方法,对应classFile中的method_info

ClassWriter类

ClassWriter的父类是ClassVisitor,因此ClassWriter类继承了visit()、visitField()、visitMethod()和visitEnd()等方法。 toByteArray方法 在ClassWriter类当中,提供了一个toByteArray()方法。这个方法的作用是将对visitXxx()的调用转换成byte[],而这些byte[]的内容就遵循ClassFile结构。 在toByteArray()方法的代码当中,通过三个步骤来得到byte[]:

  • 第一步,计算size大小。这个size就是表示byte[]的最终的长度是多少。
  • 第二步,将数据填充到byte[]当中。
  • 第三步,将byte[]数据返回。

3、使用ClassWriter类 使用ClassWriter生成一个Class文件,可以大致分成三个步骤:

  • 第一步,创建ClassWriter对象。
  • 第二步,调用ClassWriter对象的visitXxx()方法。
  • 第三步,调用ClassWriter对象的toByteArray()方法。
import org.objectweb.asm.ClassWriter;

import static org.objectweb.asm.Opcodes.*;

public class GenerateCore {
public static byte[] dump () throws Exception {
// (1) 创建ClassWriter对象
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
// (2) 调用visitXxx()方法
cw.visit();
cw.visitField();
cw.visitMethod();
cw.visitEnd(); // 注意,最后要调用visitEnd()方法

// (3) 调用toByteArray()方法
byte[] bytes = cw.toByteArray();
return bytes;
}
}
复制代码

Hanno源码分析

上面已经先回顾一下asm相关的基础知识,下面对hanno源码进行分析。主要针对三个方面:

1、如何在方法中插入Log语句。

2、如何获取对象中的field值。

3、如何获取到方法的参数


原文链接:https://juejin.cn/post/7037369790100406309?utm_source=gold_browser_extension

收起阅读 »

Android - 依赖统一管理

#前言 前段时间自己在搭建组件化框架时候遇到了多人协作 Moudle 版本依赖冲突以及重复导包和同一个包导入不同版本的情况,针对这个问题对依赖统一这块做了一次比较详细的学习和总结 目前Android依赖统一管理的方式有以下几种方式,接下来我们一起慢慢分析一下各...
继续阅读 »

#前言


前段时间自己在搭建组件化框架时候遇到了多人协作 Moudle 版本依赖冲突以及重复导包和同一个包导入不同版本的情况,针对这个问题对依赖统一这块做了一次比较详细的学习和总结


目前Android依赖统一管理的方式有以下几种方式,接下来我们一起慢慢分析一下各种方式优缺点



  1. groovy ext扩展函数(也有称之为:"循环优化")

  2. kotlin+buildSrc

  3. composing builds

  4. catalog

  5. 自定义插件+includeBuild


Groovy ext扩展函数


这种方式可能是大家最开始或者说是比较常见的一种依赖配置方式:
iShot2021-12-02 15.17.09.png


示例代码


然后在项目根build.gradle(即root路径下)


apply from:"config.gradle"


引入的方式有两种一种是循环遍历:


iShot2021-12-03 10.26.55.png


iShot2021-12-03 10.32.12.png


总结:


优点:


1:后续添加依赖不需要改动build.gradle,直接在config.gradle


2:精简了build.gradle的长度


缺点:


1:不支持代码提醒


2:不支持点击跳转


3:多moudle 开发时,不同module的依赖需要ctrl+c/v 导致开发的效率降低


kotlin+buildSrc


buildSrc


The directory buildSrc is treated as an included build. Upon discovery of the directory, Gradle automatically compiles and tests this code and puts it in the classpath of your build script. For multi-project builds there can be only one buildSrc directory, which has to sit in the root project directory. buildSrc should be preferred over script plugins as it is easier to maintain, refactor and test the code.


这是来自gradle官方文档对buildSrc的解释:


当运行 Gradle 时会检查项目中是否存在一个名为 buildSrc 的目录。然后 Gradle 会自动编译并测试这段代码,并将其放入构建脚本的类路径中, 对于多项目构建,只能有一个 buildSrc 目录,该目录必须位于根项目目录中, buildSrc 是 Gradle 项目根目录下的一个目录,它可以包含我们的构建逻辑,与脚本插件相比,buildSrc 应该是首选,因为它更易于维护、重构和测试代码


通过上面的介绍,大家或许对buildsrc 有一定理解了,那么我们就看下他怎么和kotlin一起使用达到项目统一依赖管理的


iShot2021-12-03 13.44.50.png


如上图所示我们首先创建一个名为buildSrc的module,gradle 构建的时候会先检查工程中是否有buildSrc命名的目录然后会自动编译和测试这段代码并写入到构建脚本的类路径中,所以无需在setting.gradle 做任何配置有关buildSrc的配置信息


官方的配置信息


iShot2021-12-03 14.19.27.png


iShot2021-12-03 14.21.33.png


这是我的项目中配置信息


这种方式管理依赖优点和缺点如下:


优点:


1:但这种方式支持IDE,输入代码会有提示,会自动完成,所以非常推荐使用这种方式来管理项目中的依赖包


2:支持 AndroidStudio 单击跳转


缺点:


来自gradle文档


A change in buildSrc causes the whole project to become out-of-date. Thus, when making small incremental changes, the --no-rebuild command-line option is often helpful to get faster feedback. Remember to run a full build regularly or at least when you’re done, though.


更改buildSrc会导致整个项目过时。因此,在进行小的增量更改时,--no-rebuild命令行选项通常有助于获得更快的反馈。不过,请记住定期或至少在完成后运行完整构建。


从官网的解释我们可以得出结论:


buildSrc 是对全局的所有 module 的配置依赖更新会重新构建整个项目,项目越大,重新构建的时间就越长,造成不必要的时间浪费。


阅读到这里我们可能会思考那么有没有一种方式是在部分module 需要修改依赖版本的时候而不会重新构建整个项目的方式呢,探索极致是每一个研发人员毕生所追求的,那么***"includeBuild"***这种方式应运而生


composing builds


那么我们开始一步一步实现这种方式:


1:首先创建一个library 的module <对于使用kotlin 或者 java>就要看自己的比较中意哪种语言喽


iShot2021-12-03 14.46.44.png


2:就是在library 配置插件等信息


apply plugin: 'kotlin'
apply plugin: 'java-gradle-plugin'

buildscript {
repositories {
// https://developer.aliyun.com/mvn/guide
//todo error:"Using insecure protocols with repositories, without explicit opt-in,"
google()
mavenCentral()
maven { url 'https://maven.aliyun.com/repository/public' }
maven { url 'https://maven.aliyun.com/repository/google' }
maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
}

dependencies {
// 因为使用的 Kotlin 需要需要添加 Kotlin 插件
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21"
}
}




repositories {
// 需要添加 jcenter 否则会提示找不到 gradlePlugin
repositories {
google()
mavenCentral()
maven { url 'https://maven.aliyun.com/repository/public' }
maven { url 'https://maven.aliyun.com/repository/google' }
maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
}

}

dependencies {
implementation gradleApi()
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
}


compileKotlin {
kotlinOptions {
jvmTarget = "1.8"
}
}

compileTestKotlin {
kotlinOptions {
jvmTarget = "1.8"
}
}

gradlePlugin{
plugins {
version{
// 在 app 模块需要通过 id 引用这个插件
id = 'com.bimromatic.version.plugin'
// 实现这个插件的类的路径
implementationClass = 'com.bimromatic.plugin.VersionPlugin'
}
}
}
复制代码

3:在项目路径下建立一个在.gradle 配置的类名实现Plugin 这个接口


/*
* Copyright 2009 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gradle.api;

/**
* <p>A <code>Plugin</code> represents an extension to Gradle. A plugin applies some configuration to a target object.
* Usually, this target object is a {@link org.gradle.api.Project}, but plugins can be applied to any type of
* objects.</p>
*
* @param <T> The type of object which this plugin can configure.
*/
public interface Plugin<T> {
/**
* Apply this plugin to the given target object.
*
* @param target The target object
*/
void apply(T target);
}
复制代码

4:在settings.gradle添加


iShot2021-12-03 15.01.56.png


5:在需要用的地方添加插件名


iShot2021-12-03 15.03.33.png


详细配置请移步我们的项目查看


因为时间的原因,这次项目管理依赖就讲到这里,后续会把google在孵化器期 Catalog统一配置依赖版本 讲解一下,然后我们再把各种依赖管理方式用在编辑器跑一下试试看看那种方式构建速度最快。


如果你们觉得写得不错的随手给我点个关注,后期会持续做移动端技术文章的分享,或者给我的github 点个start 后期会上传一些干货。


对了如果文章中有讲的什么不对的地方咱们评论区见,或者提上你们宝贵的issue

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

收起阅读 »

Android实战——RecyclerView条目曝光埋点

一、概要 100行代码实现recyclerview条目曝光埋点设计 二、设计思路 条目露出来一半以上视为该条目曝光。 在rv滚动过程中或者数据变更回调OnGlobalLayoutListener时,将符合条件1的条目记录在曝光列表、上传埋点集合里。 滚动状态...
继续阅读 »

一、概要


100行代码实现recyclerview条目曝光埋点设计


二、设计思路



  1. 条目露出来一半以上视为该条目曝光。

  2. 在rv滚动过程中或者数据变更回调OnGlobalLayoutListener时,将符合条件1的条目记录在曝光列表、上传埋点集合里。

  3. 滚动状态变更和OnGlobalLayoutListener回调时,且列表状态为idle状态,触发上报埋点。


三、容错性



  1. 滑动过快时,视为未曝光

  2. 数据变更时,重新检测曝光

  3. 曝光过的条目,不会重复曝光


四、接入影响



  1. 对业务代码零侵入

  2. 对列表滑动体验无影响


五、代码实现


import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import java.util.*

class RVItemExposureListener(
private val mRecyclerView: RecyclerView,
private val mExposureListener: IOnExposureListener?
) {
interface IOnExposureListener {
fun onExposure(position: Int)
fun onUpload(exposureList: List<Int>?): Boolean
}

private val mExposureList: MutableList<Int> = ArrayList()
private val mUploadList: MutableList<Int> = ArrayList()
private var mScrollState = 0

var isEnableExposure = true
private var mCheckChildViewExposure = true

private val mViewVisible = Rect()
fun checkChildExposeStatus() {
if (!isEnableExposure) {
return
}
val length = mRecyclerView.childCount
if (length != 0) {
var view: View?
for (i in 0 until length) {
view = mRecyclerView.getChildAt(i)
if (view != null) {
view.getLocalVisibleRect(mViewVisible)
if (mViewVisible.height() > view.height / 2 && mViewVisible.top < mRecyclerView.bottom) {
checkExposure(view)
}
}
}
}
}

private fun checkExposure(childView: View): Boolean {
val position = mRecyclerView.getChildAdapterPosition(childView)
if (position < 0 || mExposureList.contains(position)) {
return false
}
mExposureList.add(position)
mUploadList.add(position)
mExposureListener?.onExposure(position)
return true
}

private fun uploadList() {
if (mScrollState == RecyclerView.SCROLL_STATE_IDLE && mUploadList.size > 0 && mExposureListener != null) {
val success = mExposureListener.onUpload(mUploadList)
if (success) {
mUploadList.clear()
}
}
}

init {
mRecyclerView.viewTreeObserver.addOnGlobalLayoutListener {
if (mRecyclerView.childCount == 0 || !mCheckChildViewExposure) {
return@addOnGlobalLayoutListener
}
checkChildExposeStatus()
uploadList()
mCheckChildViewExposure = false
}
mRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(
recyclerView: RecyclerView,
newState: Int
) {
super.onScrollStateChanged(recyclerView, newState)
mScrollState = newState
uploadList()
}

override fun onScrolled(
recyclerView: RecyclerView,
dx: Int,
dy: Int
) {
super.onScrolled(recyclerView, dx, dy)
if (!isEnableExposure) {
return
}

// 大于50视为滑动过快
if (mScrollState == RecyclerView.SCROLL_STATE_SETTLING && Math.abs(dy) > 50) {
return
}
checkChildExposeStatus()
}
})
}
}


六、使用


RVItemExposureListener(yourRecyclerView, object : RVItemExposureListener.IOnExposureListener {
override fun onExposure(position: Int) {
// 滑动过程中出现的条目
Log.d("exposure-curPosition:", position.toString())
}

override fun onUpload(exposureList: List<Int>?): Boolean {
Log.d("exposure-positionList", exposureList.toString())
// 上报成功后返回true
return true
}

})

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

使用 Python 程序实现摩斯密码翻译器

算法加密解密执行摩斯密码对照表输出:.--- ..- . .--- .. -. -....- .... .- .. -.-- --- -. --.JUEJIN-HAIYONG.. .-.. --- ...- . -.-- --- ..-I LOVE YOU作...
继续阅读 »

摩斯密码是一种将文本信息作为一系列通断的音调、灯光或咔嗒声传输的方法,无需特殊设备,熟记的小伙伴即可直接翻译。它以电报发明者Samuel F. B. Morse的名字命名。

算法

算法非常简单。英语中的每个字符都被一系列“点”和“破折号”代替,或者有时只是单数的“点”或“破折号”,反之亦然。

加密

  1. 在加密的情况下,我们一次一个地从单词中提取每个字符(如果不是空格),并将其与存储在我们选择的任何数据结构中的相应摩斯密码匹配(如果您使用 python 编码,字典可以变成在这种情况下非常有用)

  2. 将摩斯密码存储在一个变量中,该变量将包含我们编码的字符串,然后我们在包含结果的字符串中添加一个空格。

  3. 在用摩斯密码编码时,我们需要在每个字符之间添加 1 个空格,在每个单词之间添加 2 个连续空格。

  4. 如果字符是空格,则向包含结果的变量添加另一个空格。我们重复这个过程,直到我们遍历整个字符串

解密

  1. 在解密的情况下,我们首先在要解码的字符串末尾添加一个空格(这将在后面解释)。

  2. 现在我们继续从字符串中提取字符,直到我们没有任何空间。

  3. 一旦我们得到一个空格,我们就会在提取的字符序列(或我们的莫尔斯电码)中查找相应的英语字符,并将其添加到将存储结果的变量中。

  4. 请记住,跟踪空间是此解密过程中最重要的部分。一旦我们得到 2 个连续的空格,我们就会向包含解码字符串的变量添加另一个空格。

  5. 字符串末尾的最后一个空格将帮助我们识别莫尔斯电码字符的最后一个序列(因为空格充当提取字符并开始解码它们的检查)。

执行

Python 提供了一种称为字典的数据结构,它以键值对的形式存储信息,这对于实现诸如摩尔斯电码之类的密码非常方便。我们可以将摩斯密码表保存在字典中,其中 (键值对)=>(英文字符-莫尔斯电码) 。明文(英文字符)代替密钥,密文(摩斯密码)形成相应密钥的值。键的值可以从字典中访问,就像我们通过索引访问数组的值一样,反之亦然。

摩斯密码对照表

# 实现摩斯密码翻译器的 Python 程序

'''
VARIABLE KEY
'cipher' -> '存储英文字符串的摩斯翻译形式'
'decipher' -> '存储摩斯字符串的英文翻译形式'
'citext' -> '存储单个字符的摩斯密码'
'i' -> '计算摩斯字符之间的空格'
'message' -> '存储要编码或解码的字符串
'''

# 表示摩斯密码图的字典
MORSE_CODE_DICT = { 'A':'.-', 'B':'-...',
'C':'-.-.', 'D':'-..', 'E':'.',
'F':'..-.', 'G':'--.', 'H':'....',
'I':'..', 'J':'.---', 'K':'-.-',
'L':'.-..', 'M':'--', 'N':'-.',
'O':'---', 'P':'.--.', 'Q':'--.-',
'R':'.-.', 'S':'...', 'T':'-',
'U':'..-', 'V':'...-', 'W':'.--',
'X':'-..-', 'Y':'-.--', 'Z':'--..',
'1':'.----', '2':'..---', '3':'...--',
'4':'....-', '5':'.....', '6':'-....',
'7':'--...', '8':'---..', '9':'----.',
'0':'-----', ', ':'--..--', '.':'.-.-.-',
'?':'..--..', '/':'-..-.', '-':'-....-',
'(':'-.--.', ')':'-.--.-'}

# 根据摩斯密码图对字符串进行加密的函数
def encrypt(message):
cipher = ''
for letter in message:
if letter != ' ':

# 查字典并添加对应的摩斯密码
# 用空格分隔不同字符的摩斯密码
cipher += MORSE_CODE_DICT[letter] + ' '
else:
# 1个空格表示不同的字符
# 2表示不同的词
cipher += ' '

return cipher

# 将字符串从摩斯解密为英文的函数
def decrypt(message):

# 在末尾添加额外空间以访问最后一个摩斯密码
message += ' '

decipher = ''
citext = ''
for letter in message:

# 检查空间
if (letter != ' '):

# 计数器来跟踪空间
i = 0

# 在空格的情况下
citext += letter

# 在空间的情况下
else:
# 如果 i = 1 表示一个新字符
i += 1

# 如果 i = 2 表示一个新词
if i == 2 :

# 添加空格来分隔单词
decipher += ' '
else:

# 使用它们的值访问密钥(加密的反向)
decipher += list(MORSE_CODE_DICT.keys())[list(MORSE_CODE_DICT
.values()).index(citext)]
citext = ''

return decipher

# 硬编码驱动函数来运行程序
def main():
message = "JUEJIN-HAIYONG"
result = encrypt(message.upper())
print (result)

message = ".--- ..- . .--- .. -. -....- .... .- .. -.-- --- -. --."
result = decrypt(message)
print (result)

message = "I LOVE YOU"
result = encrypt(message.upper())
print (result)

message = ".. .-.. --- ...- . -.-- --- ..-"
result = decrypt(message)
print (result)

# 执行主函数
if __name__ == '__main__':
main()

输出:

.--- ..- . .--- .. -. -....- .... .- .. -.-- --- -. --.
JUEJIN-HAIYONG
.. .-.. --- ...- . -.-- --- ..-
I LOVE YOU

作者:海拥
来源:https://juejin.cn/post/6990223674758397960

收起阅读 »

手写一个 ts-node 来深入理解它的原理

当我们用 Typesript 来写 Node.js 的代码,写完代码之后要用 tsc 作编译,之后再用 Node.js 来跑,这样比较麻烦,所以我们会用 ts-node 来直接跑 ts 代码,省去了编译阶段。 有没有觉得很神奇,ts-node 怎么做到的直接跑...
继续阅读 »

当我们用 Typesript 来写 Node.js 的代码,写完代码之后要用 tsc 作编译,之后再用 Node.js 来跑,这样比较麻烦,所以我们会用 ts-node 来直接跑 ts 代码,省去了编译阶段。


有没有觉得很神奇,ts-node 怎么做到的直接跑 ts 代码的?


其实原理并不难,今天我们来实现一个 ts-node 吧。


相关基础

实现 ts-node 需要 3 方面的基础知识:



  • require hook
  • repl 模块、vm 模块
  • ts compiler api

我们先学下这些基础


require hook

Node.js 当 require 一个 js 模块的时候,内部会分别调用 Module.load、 Module._extensions[‘.js’],Module._compile 这三个方法,然后才是执行。


img

同理,ts 模块、json 模块等也是一样的流程,那么我们只需要修改 Module._extensions[扩展名] 的方法,就能达到 hook 的目的:


require.extensions['.ts'] = function(module, filename) {
// 修改代码
module._compile(修改后的代码, filename);
}

比如上面我们注册了 ts 的处理函数,这样当处理 ts 模块时就会调用这个方法,所以我们在这里面做编译就可以了,这就是 ts-node 能够直接执行 ts 的原理。


repl 模块

Node.js 提供了 repl 模块可以创建 Read、Evaluate、Print、Loop 的命令行交互环境,就是那种一问一答的方式。ts-node 也支持 repl 的模式,可以直接写 ts 代码然后执行,原理就是基于 repl 模块做的扩展。


repl 的 api 是这样的: 通过 start 方法来创建一个 repl 的交互,可以指定提示符 prompt,可以自己实现 eval 的处理逻辑:


const repl = require('repl');

const r = repl.start({
prompt: '- . - > ',
eval: myEval
});

function myEval(cmd, context, filename, callback) {
// 对输入的命令做处理
callback(null, 处理后的内容);
}

repl 的执行时有一个上下文的,在这里就是 r.context,我们在这个上下文里执行代码要使用 vm 模块:


const vm = require('vm');

const res = vm.runInContext(要执行的代码, r.context);

这两个模块结合,就可以实现一问一答的命令行交互,而且 ts 的编译也可以放在 eval 的时候做,这样就实现了直接执行 ts 代码。


ts compiler api

ts 的编译我们主要是使用 tsc 的命令行工具,但其实它同样也提供了编译的 api,叫做 ts compiler api。我们做工具的时候就需要直接调用 compiler api 来做编译。


转换 ts 代码为 js 代码的 api 是这个:


const { outputText } = ts.transpileModule(ts代码, {
compilerOptions: {
strict: false,
sourceMap: false,
// 其他编译选项
}
});

当然,ts 也提供了类型检查的 api,因为参数比较多,我们后面一篇文章再做展开,这里只了解 transpileModule 的 api 就够了。


了解了 require hook、repl 和 vm、ts compiler api 这三方面的知识之后,ts-node 的实现原理就呼之欲出了,接下来我们就来实现一下。


实现 ts-node

直接执行的模式

我们可以使用 ts-node + 某个 ts 文件,来直接执行这个 ts 文件,它的原理就是修改了 require hook,也就是 Module._extensions['.ts'] 来实现的。


在 require hook 里面做 ts 的编译,然后后面直接执行编译后的 js,这样就能达到直接执行 ts 文件的效果。


所以我们重写 Module._extensions['.ts'] 方法,在里面读取文件内容,然后调用 ts.transpileModule 来把 ts 转成 js,之后调用 Module._compile 来处理编译后的 js。


这样,我们就可以直接执行 ts 模块了,具体的模块路径是通过命令行参数执行的,可以用 process.argv 来取。


const path = require('path');
const ts = require('typescript');
const fs = require('fs');

const filePath = process.argv[2];

require.extensions['.ts'] = function(module, filename) {
const fileFullPath = path.resolve(__dirname, filename);
const content = fs.readFileSync(fileFullPath, 'utf-8');

const { outputText } = ts.transpileModule(content, {
compilerOptions: require('./tsconfig.json')
});

module._compile(outputText, filename);
}

require(filePath);

我们准备一个这样的 ts 文件 test.ts:


const a = 1;
const b = 2;

function add(a: number, b: number): number {
return a + b;
}

console.log(add(a, b));

然后用这个工具 hook.js 来跑:


img

可以看到,成功的执行了 ts,这就是 ts-node 的原理。


当然,细节的逻辑还有很多,但是最主要的原理就是 require hook + ts compiler api。


repl 模式

ts-node 支持启动一个 repl 的环境,交互式的输入 ts 代码然后执行,它的原理就是基于 Node.js 提供的 repl 模块做的扩展,在自定义的 eval 函数里面做了 ts 的编译,然后使用 vm.runInContext 的 api 在 repl 的上下文中执行 js 代码。


我们也启动一个 repl 的环境,设置提示符和自定义的 eval 实现。


const repl = require('repl');

const r = repl.start({
prompt: '- . - > ',
eval: myEval
});

function myEval(cmd, context, filename, callback) {

}

eval 的实现就是编译 ts 代码为 js,然后用 vm.runInContext 来执行编译后的 js 代码,执行的 context 指定为 repl 的 context:


function myEval(cmd, context, filename, callback) {
const { outputText } = ts.transpileModule(cmd, {
compilerOptions: {
strict: false,
sourceMap: false
}
});
const res = vm.runInContext(outputText, r.context);
callback(null, res);
}

同时,我们还可以对 repl 的 context 做一些扩展,比如注入一个 who 的环境变量:


Object.defineProperty(r.context, 'who', {
configurable: false,
enumerable: true,
value: '神说要有光'
});

我们来测试下效果:


img

可以看到,执行后启动了一个 repl 环境,提示符修改成了 -.- >,可以直接执行 ts 代码,还可以访问全局变量 who。


这就是 ts-node 的 repl 模式的大概原理: repl + vm + ts compiler api。


全部代码如下:


const repl = require('repl');
const ts = require('typescript');
const vm = require('vm');

const r = repl.start({
prompt: '- . - > ',
eval: myEval
});

Object.defineProperty(r.context, 'who', {
configurable: false,
enumerable: true,
value: '神说要有光'
});

function myEval(cmd, context, filename, callback) {
const { outputText } = ts.transpileModule(cmd, {
compilerOptions: {
strict: false,
sourceMap: false
}
});
const res = vm.runInContext(outputText, r.context);
callback(null, res);
}

总结

ts-node 可以直接执行 ts 代码,不需要手动编译,为了深入理解它,我们我们实现了一个简易 ts-node,支持了直接执行和 repl 模式。


直接执行的原理是通过 require hook,也就是 Module._extensions[ext] 里通过 ts compiler api 对代码做转换,之后再执行,这样的效果就是可以直接执行 ts 代码。


repl 的原理是基于 Node.js 的 repl 模块做的扩展,可以定制提示符、上下文、eval 逻辑等,我们在 eval 里用 ts compiler api 做了编译,然后通过 vm.runInContext 在 repl 的 context 中执行编译后的 js。这样的效果就是可以在 repl 里直接执行 ts 代码。


当然,完整的 ts-node 还有很多细节,但是大概的原理我们已经懂了,而且还学到了 require hook、repl 和 vm 模块、 ts compiler api 等知识。


题外话

其实 ts-node 的原理是应一个同学的要求写的,大家有想读的 nodejs 工具的源码也可以告诉我呀(可以加我微信),无偿提供源码带读 + 简易实现的服务,不过会做一些筛选。


img
作者:zxg_神说要有光
来源:https://juejin.cn/post/7036688014206042143

收起阅读 »

前端自动化部署:借助Gitlab CI/CD实现

🛫 前端自动化部署:借助Gitlab CI/CD实现🌏 概论传统的前端部署往往都要经历:本地代码更新 => 本地打包项目 => 清空服务器相应目录 => 上传项目包至相应目录几个阶段,这些都是机械重复的步骤。对于这一过程我们往往可以通过CI/...
继续阅读 »

🛫 前端自动化部署:借助Gitlab CI/CD实现

🌏 概论

传统的前端部署往往都要经历:本地代码更新 => 本地打包项目 => 清空服务器相应目录 => 上传项目包至相应目录几个阶段,这些都是机械重复的步骤。对于这一过程我们往往可以通过CI/CD方法进行优化。所谓CI/CD,即持续集成/持续部署,以上我们所说的步骤便可以看作是持续部署的一种形态,其更详细的解释大家可以自行了解。


JenkinsTravis CI这些都是可以完成持续部署的工具。除此之外,Gitlab CI/CD也能很好的完成这一需求。下面就来详细介绍下。


🌏 核心工具

GitLab Runner

GitLab Runner是配合GitLab CI/CD完成工作的核心程序,出于性能考虑,GitLab Runner应该与Gitlab部署在不同的服务器上(Gitlab在单独的仓库服务器上,GitLab Runner在部署web应用的服务器上)。GitLab Runner在与GitLab关联后,可以在服务器上完成诸如项目拉取、文件打包、资源复制等各种命令操作。


Git

web服务器上需要安装Git来进行远程仓库的获取工作。


Node

用于在web服务器上完成打包工作。


NPM or Yarn or pnpm

用于在web服务器上完成依赖下载等工作(用yarn,pnpm亦可)。


web服务器上的所需程序

🌏 流程

这里我自己用的是centOS环境:


1. 在web服务器上安装所需工具

(1)安装Node


# 下载node包
wget https://nodejs.org/dist/v16.13.0/node-v16.13.0-linux-x64.tar.xz

# 解压Node包
tar -xf node-v16.13.0-linux-x64.tar.xz

# 在配置文件(位置多在/etc/profile)末尾添加:
export PATH=$PATH:/root/node-v16.13.0-linux-x64/bin

# 刷新shell环境:
source /etc/profile

# 查看版本(输出版本号则安装成功):
node -v

#后续安装操作,都可通过-v或者--version来查看是否成功

npm已内置在node中,如要使用yarn或,则可通过npm进行全局安装,命令与我们本地环境下的使用命令是一样的:


npm i yarn -g
#or
npm i pnpm -g

(2)安装Git


# 利用yum安装git
yum -y install git

# 查看git版本
git --version

(3)安装Gitlab Runner


# 安装程序
wget -O /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64

# 等待下载完成后分配权限
chmod +x /usr/local/bin/gitlab-runner

# 创建runner用户
useradd --comment 'test' --create-home gitlab-runner --shell /bin/bash

# 安装程序
gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner

# 启动程序
gitlab-runner start

# 安装完成后可使用gitlab-runner --version查看是否成功

2. 配置Runner及CI/CD

基本的安装操作完成后,就是最核心的阶段:Runner与CI/CD的配置。


(1)配置Gitlab Runner


首先打开待添加自动部署功能的gitlab仓库,在其中设置 > CI/CD > Runner中找到runner配置信息备用:


image.png

在web服务器中配置runner:


gitlab-runner register

>> Enter the GitLab instance URL (for example, https://gitlab.com/):
# 输入刚才获取到的gitlab仓库地址
>> Enter the registration token:
# 输入刚才获取到的token
>> Enter a description for the runner:
# 自定runner描述
>> Enter tags for the runner (comma-separated):
# 自定runner标签
>> Enter an executor: docker-ssh, docker+machine, docker-ssh+machine, docker, parallels, shell, ssh, virtualbox, kubernetes, custom:
# 选择执行器,此处我们输入shell

完整示例:image.png

(2)配置.gitlab-ci.yml


.gitlab-ci.yml文件是流水线执行的流程文件,Runner会据此完成规定的一系列流程。


我们在项目根目录中创建.gitlab-ci.yml文件,然后在其中编写内容:


# 阶段
stages:
- install
- build
- deploy

cache:
paths:
- node_modules/

# 安装依赖
install:
stage: install
# 此处的tags必须填入之前注册时自定的tag
tags:
- deploy
# 规定仅在package.json提交时才触发此阶段
only:
changes:
- package.json
# 执行脚本
script:
yarn

# 打包项目
build:
stage: build
tags:
- deploy
script:
- yarn build
# 将此阶段产物传递至下一阶段
artifacts:
paths:
- dist/

# 部署项目
deploy:
stage: deploy
tags:
- deploy
script:
# 清空网站根目录,目录请根据服务器实际情况填写
- rm -rf /www/wwwroot/stjerne/salary/*
# 复制打包后的文件至网站根目录,目录请根据服务器实际情况填写
- cp -rf ${CI_PROJECT_DIR}/dist/* /www/wwwroot/stjerne/salary/

保存并推送至gitlab后即可自动开始构建部署。


构建中可在gitlab CI/CD面板查看构建进程:


image.png

待流水线JOB完成后可前往页面查看🛫🛫🛫🛫🛫


作者:星始流年
来源:https://juejin.cn/post/7037022688493338661

收起阅读 »

聊一聊线程池和Kotlin协程

目前很多开发组都用上协程来处理异步任务了,但是有的地方协程提供的原生API还是不足以应付,比方说一些SDK提供了传入Executor的接口(以便复用调用者的线程池来执行异步任务),这时候可以用JDK提供的线程池,或者封装一下协程也可以满足需求。 协程提供了Di...
继续阅读 »

目前很多开发组都用上协程来处理异步任务了,但是有的地方协程提供的原生API还是不足以应付,比方说一些SDK提供了传入Executor的接口(以便复用调用者的线程池来执行异步任务),这时候可以用JDK提供的线程池,或者封装一下协程也可以满足需求。


协程提供了Dispatchers.DefaultDispatchers.IO 分别用于 计算密集型 任务和 IO密集型 任务,类似于RxJava的 Schedulers.computation()Schedulers.io()

但两者有所差异,比如RxJava的 Schedulers.io() 不做并发限制,而 Dispatchers.io() 做了并发限制:



It defaults to the limit of 64 threads or the number of cores (whichever is larger)



考虑到当前移动设备的CPU核心数都不超过64,所以可以认为协程的 Dispatchers.IO 的最大并发为64。

Dispatchers.Default 的并发限制为:



By default, the maximal level of parallelism used by this dispatcher is equal to the number of CPU cores, but is at least two



考虑到目前Android设备核心数都在2个以上,所以可以认为 Dispatchers.Default 的最大并发为 CPU cores。

Dispatchers.DefaultDispatchers.IO 是共享协程自己的线程池的,二者可以复用线程。

不过目前这两个Dispatchers 并未完全满足项目中的需求,有时我们需要一些自定义的并发限制,其中最常见的是串行。


RxJava有Schedulers.single() ,但这个Schedulers.single()和AsyncTask的SERAIL_EXECOTOR一样,是全局串行,不同的任务处在同一个串行队列,会相互堵塞,因而可能会引发问题。


或许也是因为这个原因,kotlin协程没有定义“Dispatchers.Single"。

对于需要串行的场景,可以这样实现:


val coroutineContext: CoroutineContext =
Executors.newSingleThreadExecutor().asCoroutineDispatcher()

这样可以实现局部的串行,但和协程的线程池是相互独立的,不能复用线程。

线程池的好处:



  1. 提高响应速度:任务到达时,无需等待线程创建即可立即执行。

  2. 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。

  3. 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。


然彼此独立创建线程池的话,会大打折扣。

如何既复用协程的线程池,又自主控制并发呢?

一个办法就是套队列来控制并发,然后还是任务还是执行在线程池之上。

AsyncTask 就是这样实现的:


private static class SerialExecutor implements Executor {
final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
Runnable mActive;

public synchronized void execute(final Runnable r) {
mTasks.offer(new Runnable() {
public void run() {
try {
r.run();
} finally {
scheduleNext();
}
}
});
if (mActive == null) {
scheduleNext();
}
}

protected synchronized void scheduleNext() {
if ((mActive = mTasks.poll()) != null) {
THREAD_POOL_EXECUTOR.execute(mActive);
}
}
}


用SerialExecutor的execute的任务会先进入队列,当mActive为空时从队列获取任务赋值给mActive然后通过线程池 THREAD_POOL_EXECUTOR执行。

当然AsyncTask 的SerialExecutor是全局唯一的,所以会有上面提到的各种任务相互堵塞的问题。可以通过创建不同是的SerialExecutor实例来达到各业务各自串行。


在Kotlin环境下,我们可以利用协程和Channel来实现:


fun Channel<Any>.runBlock(block: suspend CoroutineScope.() -> Unit) {
CoroutineScope(Dispatchers.Unconfined).launch {
send(0)
CoroutineScope(Dispatchers.IO).launch {
block()
receive()
}
}
}

// 使用方法
private val serialChannel = Channel<Any>(1)
serialChannel.runBlock {
// do somthing
}


添加Log编写测试如下:


private val a = AtomicInteger(0)
private val b = AtomicInteger(0)
fun Channel<Any>.runBlock(block: suspend CoroutineScope.() -> Unit) {
CoroutineScope(Dispatchers.Unconfined).launch {
Log.d("MyTag", "before send " + a.getAndIncrement() + getTime())
send(0)
Log.i("MyTag", "after send " + b.getAndIncrement() + getTime())
CoroutineScope(Dispatchers.Default).launch {
block()
receive()
}
}
}

private fun test() {
// 并发限制为1,串行执行任务
val channel = Channel<Any>(1)
val t1 = System.currentTimeMillis()
repeat(4) { x ->
channel.runBlock {
Thread.sleep(1000L)
Log.w("MyTag", "$x done job" + getTime())
}
}

CoroutineScope(Dispatchers.Default).launch {
while (!channel.isEmpty) {
delay(200)
}
val t2 = System.currentTimeMillis()
Log.d("MyTag", "Jobs all done, use time:" + (t2 - t1))
}
}


执行结果:



第一个任务可以顺利通过send(), 而随后的任务被suspend, 直到前面的任务执行完(执行block),调用recevie(), 然后下一个任务通过send() ……依此类推。

最终,消耗4s完成任务。


如果Channel的参数改成2,则能有两个任务可以通过send() :



最终,消耗2s完成任务。


关于参数可以参考Channel的构造函数:


public fun <E> Channel(capacity: Int = RENDEZVOUS): Channel<E> =
when (capacity) {
RENDEZVOUS -> RendezvousChannel()
UNLIMITED -> LinkedListChannel()
CONFLATED -> ConflatedChannel()
BUFFERED -> ArrayChannel(CHANNEL_DEFAULT_CAPACITY)
else -> ArrayChannel(capacity)
}

在前面的实现中, 我们关注UNLIMITED, BUFFERED 以及 capacity > 0 的情况即可:



  • UNLIMITED: 不做限制;

  • BUFFERED: 并发数由 kotlin "kotlinx.coroutines.channels.defaultBuffer"决定,目前测试得到8;

  • capacity > 0, 则并发数由 capacity 决定;

  • 特别地,当capacity = 1,为串行调度。


不过,[Dispatchers.IO] 本身有并发限制(目前版本是64),

所有对于 Channel.UNLIMITED 和 capacity > 64 的情况,和capacity=64的情况是相同的。

我们可以为不同的业务创建不同的Channel实例,从而各自控制并发且最终在协程的线程池上执行任务。

简要示意图如下:



为了简化,我们假设Dispatchers的并发限制为4。



  • 不同Channel有各自的buffer, 当任务小于capacity时进入buffer, 大于capacity时新任务被suspend。

  • Dispatchers 不断地执行任务然后调用receive(), 上面的实现中,receive并非要取什么信息,仅仅是让channel空出buffer, 好让被suspend的任务可以通过send()然后进入Dispatchers的调度。

  • 极端情况下(进入Disptachers的任务大于并发限制时),任务进入Dispatchers也不会被立即执行,这个设定可以避免开启的线程太多而陷于线程上下文频繁切换的困境。


通过Channel可以实现并发的控制,但是日常开发中有的地方并不是简单地执行个任务,而是需要一个ExecutorService或者Executor。


为此,我们可以实现一个ExecutorService。

当然了,不是直接implement ExecutorService, 而是像ThreadPoolExecutor一样继承AbstractExecutorService, 这样只需要实现几个方法即可。



最终完整代码如下:


fun Channel<Any>.runBlock(block: suspend CoroutineScope.() -> Unit) {
CoroutineScope(Dispatchers.Unconfined).launch {
send(0)
CoroutineScope(Dispatchers.IO).launch {
block()
receive()
}
}
}

class ChannelExecutorService(capacity: Int) : AbstractExecutorService() {
private val channel = Channel<Any>(capacity)

override fun execute(command: Runnable) {
channel.runBlock {
command.run()
}
}

fun isEmpty(): Boolean {
return channel.isEmpty || channel.isClosedForReceive
}

override fun shutdown() {
channel.close()
}

override fun shutdownNow(): MutableList<Runnable> {
shutdown()
return mutableListOf()
}

@ExperimentalCoroutinesApi
override fun isShutdown(): Boolean {
return channel.isClosedForSend
}

@ExperimentalCoroutinesApi
override fun isTerminated(): Boolean {
return channel.isClosedForReceive
}

override fun awaitTermination(timeout: Long, unit: TimeUnit): Boolean {
var millis = unit.toMillis(timeout)
while (!isTerminated && millis > 0) {
try {
Thread.sleep(200L)
millis -= 200L
} catch (ignore: Exception) {
}
}
return isTerminated
}
}

需要简单地控制并发的地方,直接定义Channel然后调用runBlock即可;


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

Android 编译速度提升黑科技 - RocketX

怎么做编译优化,当时说了个方案,就是编译时将所有的模块依赖修改为 aar,然后每次编译将变动的模块改成源码依赖,同时编译完成再将修改模块上传为 aar,这样可以始终做到仅有最少的模块参与源码编译,从而提升编译速度。 当然说起来轻松,做起来没有那么容易,终于有位...
继续阅读 »

怎么做编译优化,当时说了个方案,就是编译时将所有的模块依赖修改为 aar,然后每次编译将变动的模块改成源码依赖,同时编译完成再将修改模块上传为 aar,这样可以始终做到仅有最少的模块参与源码编译,从而提升编译速度。


当然说起来轻松,做起来没有那么容易,终于有位小伙伴将上述描述开发成一个开源方案了,非常值得大家学习和借鉴。


1.背景描述


在项目体量越来越大的情况下,编译速度也随着增长,有时候一个修改需要等待长达好几分钟的编译时间。


基于这种普遍的情况,推出了 RocketX ,通过在编译流程动态修改项目依赖关系, 动态 替换 module 为 aar,做到只编译改动模块,其他模块不参与编译,无需改动原有项目任何代码,提高全量编译的速度。


2.效果展示


2.1、测试项目介绍

目标项目一共 3W+ 个类与资源文件,全量编译 4min 左右(测试使用 18 年 mbp 8代i7 16g)。


通过 RocketX 全量增速之后的效果(每一个操作取 3 次平均值)。


image.png


项目依赖关系如下图,app 依赖 bm 业务模块,bm 业务模块依赖顶层 base/comm 模块。


image.png


依赖关系


• 当 base/comm 模块改动,底部的所有模块都必须参与编译。因为 app/bmxxx 模块可能使用了 base 模块中的接口或变量等,并且不知道是否有改动到。(那么速度就非常慢)


• 当 bmDiscover 做了改动,只需要 app 模块和 bmDiscover 两个模块参与编译。(速度较快)


• rx(RocketX) 在无论哪一个模块的编译速度基本都是在控制在 30s 左右,因为只编译 app 和 改动的模块,其他模块是 aar 包不参与编译。


顶层模块速度提升 300%+


3.思路问题分析与模块搭建


3.1、思路问题分析

需要通过 gradle plugin 的形式动态修改没有改动过的 module 依赖为 相对应的 aar 依赖,如果 module 改动,退化成 project 工程依赖,这样每次只有改动的 module 和 app 两个模块编译。


需要把 implement/api moduleB,修改为implement/api aarB。


需要构建 local maven 存储未被修改的 module 对应的 aar。(也可以通过 flatDir 代替速度更快)


编译流程启动,需要找到哪一个 module 做了修改。


需要遍历每一个 module 的依赖关系进行置换, module 依赖怎么获取?一次性能获取到所有模块依赖,还是分模块各自回调?修改其中一个模块依赖关系会阻断后面模块依赖回调?


每一个 module 换变成 aar 之后,自身依赖的 child 依赖 (网络依赖,aar),给到 parent module (如何找到所有 parent module) ? 还是直接给 app module ? 有没有 app 到 module 依赖断掉的风险?这里需要出一个技术方案。


需要hook 编译流程,完成后置换 loacal maven 中被修改的 aar。


提供 AS 状态栏 button, 实现开启关闭功能,加速编译还是让开发者使用已经习惯性的三角形 run 按钮。


3.2、模块搭建

依照上面的分析,虽然问题很多,但是大致可以把整个项目分成以下几块:


image.png


4.问题解决与实现


4.1、implement 源码实现入口在 DynamicAddDependencyMethods 中的 tryInvokeMethod 方法。他是一个动态语言的 methodMissing 功能。

tryInvokeMethod 代码分析:


 public DynamicInvokeResult tryInvokeMethod(String name, Object... arguments) {       //省略部分代码 ...       return DynamicInvokeResult.found(this.dependencyAdder.add(configuration, normalizedArgs.get(0), (Closure)null)); }
复制代码

dependencyAdder 实现是一个 DirectDependencyAdder。


private class DirectDependencyAdder implements DependencyAdder<Dependency> {    private DirectDependencyAdder() {    }    public Dependency add(Configuration configuration, Object dependencyNotation, @Nullable Closure configureAction) {        return DefaultDependencyHandler.this.doAdd(configuration, dependencyNotation, configureAction);    }}
复制代码

最后是在 DefaultDependencyHandler.this.doAdd 进行添加进去,而 DefaultDependencyHandler 在 project可以获取。


  DependencyHandler getDependencies(); 
复制代码

通过以上的分析,添加相对应的 aar/jar 可以通过以下代码实现。


fun addAarDependencyToProject(aarName: String, configName: String, project: Project) {    //添加 aar 依赖 以下代码等同于 api/implementation/xxx (name: 'libaccount-2.0.0', ext: 'aar'),源码使用 linkedMap    if (!File(FileUtil.getLocalMavenCacheDir() + aarName + ".aar").exists()) return    val map = linkedMapOf<String, String>()    map.put("name", aarName)    map.put("ext", "aar")    // TODO: 2021/11/5 改变依赖 这里后面需要修改成    //project.dependencies.add(configName, "com.${project.name}:${project.name}:1.0")    project.dependencies.add(configName, map)}
复制代码

4.2、localMave 优先使用 flatDir 实现通过指定一个缓存目录把生成 aar/jar 包丢进去,依赖修改时候通过找寻进行替换。

fun flatDirs() {    val map = mutableMapOf<String, File>()    map.put("dirs", File(getLocalMavenCacheDir()))    appProject.rootProject.allprojects {        it.repositories.flatDir(map)    }}
复制代码

4.3、编译流程启动,需要找到哪一个 module做了修改。

使用遍历整个项目的文件的 lastModifyTime 去做实现。


以每一个 module 为一个粒度,递归遍历当前 module 的文件,把每个文件的 lastModifyTime 整合计算得出一个唯一标识 countTime。


通过 countTime 与上一次的作对比,相同说明没改动,不同则改动. 并需要同步计算后的 countTime 到本地缓存中。


整体 3W 个文件耗时 1.2s 可以接受。


4.4、 module 依赖关系获取。

通过以下代码可以找到生成整个项目的依赖关系图时机,并在此处生成依赖图解析器。


 project.gradle.addListener(DependencyResolutionListener listener)
复制代码

4.5、 module 依赖关系 project 替换成 aar 技术方案

每一个 module 依赖关系替换的遍历顺序是无序的,所以技术方案需要支持无序的替换。


目前使用的方案是:如果当前模块 A 未改动,需要把 A 通过 localMaven 置换成 A.aar,并把 A.aar 以及 A 的 child 依赖,给到第一层的 parent module 即可。(可能会质疑如果 parent module 也是 aar 怎么办,其实这块也是没有问题的,这里就不展开说了,篇幅太长)


为什么要给到 parent 不能直接给到 app ,下图一个简单的示例如果 B.aar 不给 A 模块的话,A 使用 B 模块的接口不见了,会导致编译不过。


image.png


给出整体项目替换的技术方案演示:


image.png


4.5、hook 编译流程,完成后置换 loacal maven 中被修改的 aar。

点击三角形 run,执行的命令是 app:assembleDebug , 需要在 assembleDebug 后面补一个 uploadLocalMavenTask, 通过 finalizedBy 把我们的 task 运行起来去同步修改后的 aar


4.6、提供 AS 状态栏 button,小火箭按钮一个喷火一个没有喷火,代表 enable/disable , 一个 扫把clean rockectx 的缓存。

image.png


5一天一个小惊喜


5.1、发现点击 run 按钮 ,执行的命令是 app:assembleDebug ,各个子 module 在 output 并没有打包出 aar。

解决:通过研究 gradle 源码发现打包是由 bundleFlavor{Flavor}{BuildType}Aar 这个task执行出来,那么只需要将各个模块对应的 task 找到并注入到 app:assembleDebug 之后运行即可。


5.2、发现运行起来后存在多个 jar 包重复问题。

解决:implementation fileTree(dir: "libs", include: ["*.jar"]) jar 依赖不能交到 parent module,jar 包会打进 aar 中的lib 可直接剔除。通过以下代码可以判断:


// 这里的依赖是以下两种: 无需添加在 parent ,因为 jar 包直接进入 自身的 aar 中的libs 文件夹//    implementation rootProject.files("libs/xxx.jar")//    implementation fileTree(dir: "libs", include: ["*.jar"])childDepency.files is DefaultConfigurableFileCollection || childDepency.files is DefaultConfigurableFileTree
复制代码

5.3、发现 aar/jar 存在多种依赖方式。

implementation (name: 'libXXX', ext: 'aar')


implementation files("libXXX.aar")


解决:使用第一种,第二种会合并进aar,导致类重复问题.


5.4、发现 aar 新姿势依赖。

configurations.maybeCreate("default")artifacts.add("default", file('lib-xx.aar'))
复制代码

上面代码把 aar 做了一个单独的 module 给到其他 module 依赖,default config 其实是 module 最终输出 aar 的持有者,default config 可以持有一个 列表的aar ,所以把 aar 手动添加到 default config,也相当于当前 module 打包出来的产物。


解决:通过 childProject.configurations.maybeCreate("default").artifacts 找到所有添加进来的 aar ,单独发布 localmaven。


5.5、发现 android module 打包出来可以是 jar。

解决:通过找到名字叫做 jar 的task,并且在 jar task 后面注入 uploadLocalMaven task。


5.6、发现 arouter 有 bug,transform 没有通过 outputProvider.deleteAll() 清理旧的缓存。

解决:详情查看 issue,结果arouter 问题是解决了,代码也是合并了。但并没有发布新的插件版本到 mavenCentral,于是先自行帮 arouter 解决一下。


github.com/alibaba/ARo…



关注我,每天分享知识干货!

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

Android CameraX结合LibYUV和GPUImage自定义相机滤镜

前言 之前使用Camera实现了一个自定义相机滤镜(Android自定义相机滤镜 ),但是运行起来有点卡顿,这次用Camerax来实现一样的效果发现很流畅,在此记录一下,也希望能帮到有需要的同学。 实现效果 实现步骤 1.引入依赖库 这里我引入的依赖库有Ca...
继续阅读 »

前言


之前使用Camera实现了一个自定义相机滤镜(Android自定义相机滤镜 ),但是运行起来有点卡顿,这次用Camerax来实现一样的效果发现很流畅,在此记录一下,也希望能帮到有需要的同学。


实现效果



实现步骤


1.引入依赖库

这里我引入的依赖库有CameraXGPUImage(滤镜库)、Utilcodex(一款好用的工具类)


// CameraX core library using camera2 implementation
    implementation "androidx.camera:camera-camera2:1.0.1"
// CameraX Lifecycle Library
    implementation "androidx.camera:camera-lifecycle:1.0.1"
// CameraX View class
    implementation "androidx.camera:camera-view:1.0.0-alpha27"

    implementation'jp.co.cyberagent.android.gpuimage:gpuimage-library:1.4.1'
    implementation 'com.blankj:utilcodex:1.30.6'

2.引入libyuv

这里我用的是这个案例(github.com/theeasiestw…



3.编写CameraX预览代码

布局代码如下


<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <androidx.camera.view.PreviewView
        android:id="@+id/viewFinder"
        android:layout_width="0dp"
        android:layout_height="0dp" />
</FrameLayout>

Activity中开启相机预览代码如下,基本都是Google官方提供的案例代码


class MainActivity : AppCompatActivity() {
    private lateinit var cameraExecutor: ExecutorService
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        cameraExecutor = Executors.newSingleThreadExecutor()
        // Request camera permissions
        if (allPermissionsGranted()) {
            startCamera()
        } else {
            ActivityCompat.requestPermissions(
                this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
        }
    }

    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(
            baseContext, it) == PackageManager.PERMISSION_GRANTED
    }

    override fun onRequestPermissionsResult(
        requestCode: Int, permissions: Array<String>, grantResults:
        IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                startCamera()
            } else {
                Toast.makeText(this,
                    "Permissions not granted by the user.",
                    Toast.LENGTH_SHORT).show()
                finish()
            }
        }
    }
    private fun startCamera() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        cameraProviderFuture.addListener(Runnable {
            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
            val preview = Preview.Builder()
                .build()
                .also {
                    it.setSurfaceProvider(viewFinder.surfaceProvider)
                }
            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
            try {
                cameraProvider.unbindAll()
                cameraProvider.bindToLifecycle(
                    this, cameraSelector, preview)
            } catch(exc: Exception) {
                Log.e(TAG, "Use case binding failed", exc)
            }
        }, ContextCompat.getMainExecutor(this))
    }

    override fun onDestroy() {
        super.onDestroy()
        cameraExecutor.shutdown()
    }

    companion object {
        private const val TAG = "CameraXBasic"
        private const val REQUEST_CODE_PERMISSIONS = 10
        private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)

    }
}

到这里就可以实现相机预览了



4.增加相机数据回调

我们要增加滤镜效果就必须对相机的数据进行操作,这里我们通过获取相机数据回调来获取可修改的数据


val imageAnalyzer = ImageAnalysis.Builder()
                //设置回调数据的比例为16:9
                .setTargetAspectRatio(AspectRatio.RATIO_16_9)
                .build()
                .also {
                    it.setAnalyzer(cameraExecutor,this@MainActivity)
                }

这里我们还需要进行绑定



除此之外我们还需要在Activity中实现ImageAnalysis.Analyzer接口,数据的获取就在此接口的回调方法中获取,如下所示,其中ImageProxy就包含了图像数据


override fun analyze(image: ImageProxy) {

}

5.对回调数据进行处理

我们在相机数据回调的方法中对图像进行处理并添加滤镜,当然在此之前我们还需要创建GPUImage对象并设置滤镜类型


private var bitmap:Bitmap? = null
private var gpuImage:GPUImage? = null
//创建GPUImage对象并设置滤镜类型,这里我使用的是素描滤镜
private fun initFilter() {
        gpuImage = GPUImage(this)
        gpuImage!!.setFilter(GPUImageSketchFilter())
    }
@SuppressLint("UnsafeOptInUsageError")
    override fun analyze(image: ImageProxy) {
        //将Android的YUV数据转为libYuv的数据
        var yuvFrame = yuvUtils.convertToI420(image.image!!)
        //对图像进行旋转(由于回调的相机数据是横着的因此需要旋转90度)
        yuvFrame = yuvUtils.rotate(yuvFrame, 90)
        //根据图像大小创建Bitmap
        bitmap = Bitmap.createBitmap(yuvFrame.width, yuvFrame.height, Bitmap.Config.ARGB_8888)
        //将图像转为Argb格式的并填充到Bitmap上
        yuvUtils.yuv420ToArgb(yuvFrame,bitmap!!)
        //利用GpuImage给图像添加滤镜
        bitmap = gpuImage!!.getBitmapWithFilterApplied(bitmap)
        //由于这不是UI线程因此需要在UI线程更新UI
        img.post {
            img.setImageBitmap(bitmap)
            //关闭ImageProxy,才会回调下一次的数据
            image.close()
        }

    }

6.拍摄照片

这里我们加一个拍照的按钮


<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <androidx.camera.view.PreviewView
        android:id="@+id/viewFinder"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    <ImageView
        android:id="@+id/img"
        android:scaleType="centerCrop"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
    <Button
        android:id="@+id/bt_takepicture"
        android:layout_gravity="center_horizontal|bottom"
        android:layout_marginBottom="100dp"
        android:text="拍照"
        android:layout_width="70dp"
        android:layout_height="70dp"/>
</FrameLayout>

然后我们在Activity中添加拍照的逻辑,其实就是将Bitmap转为图片保存到SD卡,这里我们使用了之前引入的Utilcodex工具,当我们点击按钮的时候isTakePhoto 会变为true,然后在相机的回调中就会进行保存图片的处理


bt_takepicture.setOnClickListener {
            isTakePhoto = true
        }

并且我们加入变量控制,在拍照的时候不处理回调数据


@SuppressLint("UnsafeOptInUsageError")
    override fun analyze(image: ImageProxy) {
        if(!isTakePhoto){
            //将Android的YUV数据转为libYuv的数据
            var yuvFrame = yuvUtils.convertToI420(image.image!!)
            //对图像进行旋转(由于回调的相机数据是横着的因此需要旋转90度)
            yuvFrame = yuvUtils.rotate(yuvFrame, 90)
            //根据图像大小创建Bitmap
            bitmap = Bitmap.createBitmap(yuvFrame.width, yuvFrame.height, Bitmap.Config.ARGB_8888)
            //将图像转为Argb格式的并填充到Bitmap上
            yuvUtils.yuv420ToArgb(yuvFrame,bitmap!!)
            //利用GpuImage给图像添加滤镜
            bitmap = gpuImage!!.getBitmapWithFilterApplied(bitmap)
            //由于这不是UI线程因此需要在UI线程更新UI
            img.post {
                img.setImageBitmap(bitmap)
                if(isTakePhoto){
                    takePhoto()
                }
                //关闭ImageProxy,才会回调下一次的数据
                image.close()
            }
        }else{
            image.close()
        }
    }
 /**
     * 拍照
     */
    private fun takePhoto() {
        Thread{
            val filePath = File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),"${System.currentTimeMillis()}save.png")
            ImageUtils.save(bitmap,filePath.absolutePath,Bitmap.CompressFormat.PNG)
            ToastUtils.showShort("拍摄成功")
            isTakePhoto = false
        }.start()
    }

效果如下



保存的图片在如下目录



保存的图片如下



只有不断的学习进步,才能不被时代淘汰。关注我,每天分享知识干货!


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

探究Android属性动画执行过程

1.引言属性动画作为Android动画功能的一个重要组成部分,可以实现很多有趣的动画效果,理解属性动画的执行过程有助于我们更好地使用属性动画去实现需求。本文将从源码的角度去探索属性动画的实现过程,加深大家对其的认知和理解。2.属性动画相关的类2.1 Value...
继续阅读 »

1.引言

属性动画作为Android动画功能的一个重要组成部分,可以实现很多有趣的动画效果,理解属性动画的执行过程有助于我们更好地使用属性动画去实现需求。本文将从源码的角度去探索属性动画的实现过程,加深大家对其的认知和理解。

2.属性动画相关的类

2.1 ValueAnimator

这个类是实现属性动画的一个重要的类,通过ValueAnimator.ofFloat()、ValueAnimator.ofInt()、ValueAnimator.ofObject()、ValueAnimator.ofArgb()、ValueAnimator.ofPropertyValuesHolder()等方法可以获得ValueAnimator的对象,然后可以通过对这个对象的操作去实现动画。使用ValueAnimator实现属性动画,需要实现ValueAnimator.AnimatorUpdateListener()接口,并在onAnimationUpdate()方法内为要添加动画的对象设置属性值。

2.2 ObjectAnimator

ObjectAnimator是ValueAnimator的子类,可以操作目标对象的动画属性,这个类的构造函数支持采用参数的形式传入要使用动画的目标对象和属性名。

3.属性动画的实现过程

ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(iv, "alpha", 1.0f, 0f);
objectAnimator.setDuration(3000);
objectAnimator.start();

这是一段简单的代码,它使用属性动画实现了一张图片的透明度渐变的效果,我们从这一段代码入手,去分析属性动画的实现过程。

3.1 创建属性动画

/**
* target:添加动画效果的目标对象
* propertyName:动画效果的属性名
* values:动画将会在这个时间之间执行的数值集合
*/
public static ObjectAnimator ofFloat(Object target, String propertyName, float... values){
ObjectAnimator anim = new ObjectAnimator(target, propertyName);
anim.setFloatValues(values);
return anim;
}

这个方法返回了一个属性动画对象,第一个参数是产生动画效果的目标对象,第二个参数是属性名,目标对象的属性名应该有与之对应的set()方法,例如我们传入属性名"alpha",那么这个目标对象也应该有setAlpha()方法。参数values传一个值的时候,这个值是动画的结束值,传两个数值的时候,第一个值是开始值,第二个值是结束值,多于两个值的时候,第一个值是开始值,最后一个值是结束值。

private ObjectAnimator(Object target, String propertyName) {
setTarget(target);
setPropertyName(propertyName);
}

这个是属性动画的构造函数,里面执行了两个方法setTarget(target)和setPropertyName(propertyName)。

@Override
public void setTarget(@Nullable Object target) {
final Object oldTarget = getTarget();
if (oldTarget != target) {
if (isStarted()) {
cancel();
}
mTarget = target == null ? null : new WeakReference<Object>(target);
// New target should cause re-initialization prior to starting
mInitialized = false;
}
}
public void setPropertyName(@NonNull String propertyName) {
// mValues could be null if this is being constructed piecemeal. Just record the
// propertyName to be used later when setValues() is called if so.
if (mValues != null) {
PropertyValuesHolder valuesHolder = mValues[0];
String oldName = valuesHolder.getPropertyName();
valuesHolder.setPropertyName(propertyName);
mValuesMap.remove(oldName);
mValuesMap.put(propertyName, valuesHolder);
}
mPropertyName = propertyName;
// New property/values/target should cause re-initialization prior to starting
mInitialized = false;
}

mValues是一个PropertyValuesHolder数组,PropertyValuesHolder持有动画的属性名和属性值信息,mValuesMap是一个hashmap数组,用来管理PropertyValuesHolder对象,在调用getAnimatedValue(String)方法的时候,这个map通过属性名去查找动画执行的数值。当mValues不为空的时候,将属性名信息放入mValuesMap。

//ObjectAnimator
@Override
public void setFloatValues(float... values) {
if (mValues == null || mValues.length == 0) {
// No values yet - this animator is being constructed piecemeal. Init the values with
// whatever the current propertyName is
if (mProperty != null) {
setValues(PropertyValuesHolder.ofFloat(mProperty, values));
} else {
setValues(PropertyValuesHolder.ofFloat(mPropertyName, values));
}
} else {
super.setFloatValues(values);
}
}

mValues为null或者数组元素个数为0的时候,调用其父类ValueAnimator的setValues()方法,在setValues()内执行了初始化mValues和mValuesMap的操作,并将PropertyValuesHolder放入mValuesMap。当mValues不为null且元素个数不为0的时候,调用其父类ValueAnimator的setFloatValues()方法,在setFloatValues()方法内满足条件又会调用到PropertyValuesHolder的setFloatValues()方法。

//PropertyValuesHolder
public void setFloatValues(float... values) {
mValueType = float.class;
mKeyframes = KeyframeSet.ofFloat(values);
}

这里的mValueType指的是提供的值的类型,mKeyframes是定义这个动画的关键帧集合。

//KeyframeSet
public static KeyframeSet ofFloat(float... values) {
boolean badValue = false;
int numKeyframes = values.length;
FloatKeyframe keyframes[] = new FloatKeyframe[Math.max(numKeyframes,2)];
if (numKeyframes == 1) {
keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f);
keyframes[1] = (FloatKeyframe) Keyframe.ofFloat(1f, values[0]);
if (Float.isNaN(values[0])) {
badValue = true;
}
} else {
keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f, values[0]);
for (int i = 1; i < numKeyframes; ++i) {
keyframes[i] =
(FloatKeyframe) Keyframe.ofFloat((float) i / (numKeyframes - 1), values[i]);
if (Float.isNaN(values[i])) {
badValue = true;
}
}
}
if (badValue) {
Log.w("Animator", "Bad value (NaN) in float animator");
}
return new FloatKeyframeSet(keyframes);
}

在这个方法内新建了一个FloatKeyframe数组,数组的元素至少为2个,FloatKeyframe是Keyframe的内部子类,持有这个动画的时间值对,Keyframe类被ValueAnimator用来定义整个动画过程中动画目标的数值,当时间从一帧到另一帧,目标对象的值也会从上一帧的值运动到下一帧的值。

/**
* fraction:取值范围0到1之间,表示全部动画时长中已经执行的时间部分
* value:关键帧中与时间相对应的数值
*/
public static Keyframe ofFloat(float fraction, float value) {
return new FloatKeyframe(fraction, value);
}

此方法使用给定的时间和数值创建一个关键帧对象,到这里,属性动画的创建过程基本完成。

3.2 属性动画执行过程

//ObjectAnimator
@Override
public void start() {
AnimationHandler.getInstance().autoCancelBasedOn(this);
if (DBG) {
Log.d(LOG_TAG, "Anim target, duration: " + getTarget() + ", " + getDuration());
for (int i = 0; i < mValues.length; ++i) {
PropertyValuesHolder pvh = mValues[i];
Log.d(LOG_TAG, " Values[" + i + "]: " +
pvh.getPropertyName() + ", " + pvh.mKeyframes.getValue(0) + ", " +
pvh.mKeyframes.getValue(1));
}
}
super.start();
}

在代码中调用objectAnimator.start()的时候动画开始执行,内部调用了其父类ValueAnimator的start()方法。

//ValueAnimator
private void start(boolean playBackwards) {
if (Looper.myLooper() == null) {
throw new AndroidRuntimeException("Animators may only be run on Looper threads");
}
mReversing = playBackwards;
mSelfPulse = !mSuppressSelfPulseRequested;
// Special case: reversing from seek-to-0 should act as if not seeked at all.
if (playBackwards && mSeekFraction != -1 && mSeekFraction != 0) {
if (mRepeatCount == INFINITE) {
// Calculate the fraction of the current iteration.
float fraction = (float) (mSeekFraction - Math.floor(mSeekFraction));
mSeekFraction = 1 - fraction;
} else {
mSeekFraction = 1 + mRepeatCount - mSeekFraction;
}
}
mStarted = true;
mPaused = false;
mRunning = false;
mAnimationEndRequested = false;
// Resets mLastFrameTime when start() is called, so that if the animation was running,
// calling start() would put the animation in the
// started-but-not-yet-reached-the-first-frame phase.
mLastFrameTime = -1;
mFirstFrameTime = -1;
mStartTime = -1;
addAnimationCallback(0);

if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {
// If there's no start delay, init the animation and notify start listeners right away
// to be consistent with the previous behavior. Otherwise, postpone this until the first
// frame after the start delay.
startAnimation();
if (mSeekFraction == -1) {
// No seek, start at play time 0. Note that the reason we are not using fraction 0
// is because for animations with 0 duration, we want to be consistent with pre-N
// behavior: skip to the final value immediately.
setCurrentPlayTime(0);
} else {
setCurrentFraction(mSeekFraction);
}
}
}

在这个方法内进行了一些赋值操作,addAnimationCallback(0)和startAnimation()是比较重要的操作。

//ValueAnimator
private void addAnimationCallback(long delay) {
if (!mSelfPulse) {
return;
}
getAnimationHandler().addAnimationFrameCallback(this, delay);
}

这个方法内执行了AnimationHandler的addAnimationFrameCallback()方法注册回调,我们继续看看addAnimationFrameCallback()方法。

//AnimationHandler
public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
if (mAnimationCallbacks.size() == 0) {
getProvider().postFrameCallback(mFrameCallback);
}
if (!mAnimationCallbacks.contains(callback)) {
mAnimationCallbacks.add(callback);
}

if (delay > 0) {
mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
}
}

这个方法添加了一个AnimationFrameCallback回调,AnimationFrameCallback是AnimationHandler的一个内部接口,其中有两个重要的方法doAnimationFrame()和commitAnimationFrame()。

//AnimationHandler
interface AnimationFrameCallback {
boolean doAnimationFrame(long frameTime);

void commitAnimationFrame(long frameTime);
}

AnimationFrameCallback是可以收到动画执行时间和帧提交时间通知的回调,内有两个方法,doAnimationFrame()和commitAnimationFrame()。

//AnimationHandler
private class MyFrameCallbackProvider implements AnimationFrameCallbackProvider {

final Choreographer mChoreographer = Choreographer.getInstance();

@Override
public void postFrameCallback(Choreographer.FrameCallback callback) {
mChoreographer.postFrameCallback(callback);
}

@Override
public void postCommitCallback(Runnable runnable) {
mChoreographer.postCallback(Choreographer.CALLBACK_COMMIT, runnable, null);
}

@Override
public long getFrameTime() {
return mChoreographer.getFrameTime();
}

@Override
public long getFrameDelay() {
return Choreographer.getFrameDelay();
}

@Override
public void setFrameDelay(long delay) {
Choreographer.setFrameDelay(delay);
}
}

前面的getProvider()方法获得了MyFrameCallbackProvider的一个实例,MyFrameCallbackProvider是AnimationHandler的一个内部类,实现了AnimationFrameCallbackProvider接口,使用Choreographer作为计时脉冲的提供者,去发送帧回调。Choreographer从显示器子系统获得时间脉冲,postFrameCallback()方法发送帧回调。

//AnimationHandler
public interface AnimationFrameCallbackProvider {
void postFrameCallback(Choreographer.FrameCallback callback);
void postCommitCallback(Runnable runnable);
long getFrameTime();
long getFrameDelay();
void setFrameDelay(long delay);
}
//AnimationHandler
private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
doAnimationFrame(getProvider().getFrameTime());
if (mAnimationCallbacks.size() > 0) {
getProvider().postFrameCallback(this);
}
}
};

在这个回调内执行了doAnimationFrame()方法,如果mAnimationCallbacks的个数大于0,AnimationFrameCallbackProvider就继续发送帧回调,继续重复执行doAnimationFrame()。

//AnimationHandler   
private void doAnimationFrame(long frameTime) {
long currentTime = SystemClock.uptimeMillis();
final int size = mAnimationCallbacks.size();
for (int i = 0; i < size; i++) {
final AnimationFrameCallback callback = mAnimationCallbacks.get(i);
if (callback == null) {
continue;
}
if (isCallbackDue(callback, currentTime)) {
callback.doAnimationFrame(frameTime);
if (mCommitCallbacks.contains(callback)) {
getProvider().postCommitCallback(new Runnable() {
@Override
public void run() {
commitAnimationFrame(callback, getProvider().getFrameTime());
}
});
}
}
}
cleanUpList();
}

在这个方法内开启了一个循环,里面执行了callback.doAnimationFrame(),这个操作会触发ValueAnimator类中的doAnimationFrame()。

//ValueAnimator
private void startAnimation() {
if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, getNameForTrace(),
System.identityHashCode(this));
}

mAnimationEndRequested = false;
initAnimation();
mRunning = true;
if (mSeekFraction >= 0) {
mOverallFraction = mSeekFraction;
} else {
mOverallFraction = 0f;
}
if (mListeners != null) {
notifyStartListeners();
}
}

startAnimation()方法内调用了initAnimation()初始化动画。

//ValueAnimator
public final boolean doAnimationFrame(long frameTime) {
//省略部分代码
...
final long currentTime = Math.max(frameTime, mStartTime);
boolean finished = animateBasedOnTime(currentTime);

if (finished) {
endAnimation();
}
return finished;
}

这个方法在执行动画的过程中会被多次调用,其中重要的操作是animateBasedOnTime(currentTime)。

//ValueAnimator
boolean animateBasedOnTime(long currentTime) {
boolean done = false;
if (mRunning) {
final long scaledDuration = getScaledDuration();
final float fraction = scaledDuration > 0 ?
(float)(currentTime - mStartTime) / scaledDuration : 1f;
final float lastFraction = mOverallFraction;
final boolean newIteration = (int) fraction > (int) lastFraction;
final boolean lastIterationFinished = (fraction >= mRepeatCount + 1) &&
(mRepeatCount != INFINITE);
if (scaledDuration == 0) {
// 0 duration animator, ignore the repeat count and skip to the end
done = true;
} else if (newIteration && !lastIterationFinished) {
// Time to repeat
if (mListeners != null) {
int numListeners = mListeners.size();
for (int i = 0; i < numListeners; ++i) {
mListeners.get(i).onAnimationRepeat(this);
}
}
} else if (lastIterationFinished) {
done = true;
}
mOverallFraction = clampFraction(fraction);
float currentIterationFraction = getCurrentIterationFraction(
mOverallFraction, mReversing);
animateValue(currentIterationFraction);
}
return done;
}

animateBasedOnTime()方法计算了已经执行的动画时长和动画分数,并调用animateValue()方法计算动画值。

//ValueAnimator
void animateValue(float fraction) {
fraction = mInterpolator.getInterpolation(fraction);
mCurrentFraction = fraction;
int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
mValues[i].calculateValue(fraction);
}
if (mUpdateListeners != null) {
int numListeners = mUpdateListeners.size();
for (int i = 0; i < numListeners; ++i) {
mUpdateListeners.get(i).onAnimationUpdate(this);
}
}
}

ValueAnimator的animateValue()方法内部首先根据动画分数得到插值分数,再根据插值分数计算动画值,并调用了AnimatorUpdateListener的onAnimationUpdate()方法通知更新。

//ObjectAnimator
@Override
void animateValue(float fraction) {
final Object target = getTarget();
if (mTarget != null && target == null) {
// We lost the target reference, cancel and clean up. Note: we allow null target if the
/// target has never been set.
cancel();
return;
}

super.animateValue(fraction);
int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
mValues[i].setAnimatedValue(target);
}
}

ObjectAnimator的animateValue()方法不仅调用了父类的animateValue()方法,还在循环内调用了PropertyValuesHolder的setAnimatedValue()方法,传入的参数是产生动画效果的目标对象。

//PropertyValuesHolder
@Override
void setAnimatedValue(Object target) {
if (mFloatProperty != null) {
mFloatProperty.setValue(target, mFloatAnimatedValue);
return;
}
if (mProperty != null) {
mProperty.set(target, mFloatAnimatedValue);
return;
}
if (mJniSetter != 0) {
nCallFloatMethod(target, mJniSetter, mFloatAnimatedValue);
return;
}
if (mSetter != null) {
try {
mTmpValueArray[0] = mFloatAnimatedValue;
mSetter.invoke(target, mTmpValueArray);
} catch (InvocationTargetException e) {
Log.e("PropertyValuesHolder", e.toString());
} catch (IllegalAccessException e) {
Log.e("PropertyValuesHolder", e.toString());
}
}
}

在PropertyValuesHolder的setAnimatedValue()方法内部,先通过JNI去修改目标对象的属性值,如果通过JNI找不到对应的方法,则通过使用反射机制修改目标对象的属性值。

4.总结

属性动画的功能相当强大,可以为视图对象和非视图对象添加动画效果,属性动画是通过改变要添加动画的目标对象的属性值实现的,ValueAnimator基于动画时长和已经执行的时长计算得出动画分数,然后根据设置的时间插值器TimeInterpolator计算得出动画的插值分数,再调用对应的估值器TypeEvaluator根据插值分数、起始值和结束值计算得出对象的属性值,ObjectAnimator类在计算出动画的新值后自动地更新对象的属性值,ValueAnimator类则需要手动地去设置对象的属性值。


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

收起阅读 »

为什么我不用 Typescript

前言 我算是久仰 Typescript 的大名了,因而之前就想学习,但是一直没有抽出时间来看看它。直到最近有一天我在知乎上被邀请回答了 一个问题 —— 一个我以为的中学生问怎么样提升他的开源仓库。我点进去,先是被惊艳到了;然后发现,他用的是 Typescrip...
继续阅读 »

前言


我算是久仰 Typescript 的大名了,因而之前就想学习,但是一直没有抽出时间来看看它。直到最近有一天我在知乎上被邀请回答了 一个问题 —— 一个我以为的中学生问怎么样提升他的开源仓库。我点进去,先是被惊艳到了;然后发现,他用的是 Typescript。我顿时感觉我似乎落后了,于是鼓起劲,开始学起了 Typescript。


但是我学了下,再用了下,发现它没有像被吹的那么神。虽说是 Javascript 的超集,也的确有些地方挺好的,但是还是不足够改变我使用 Javascript 编程。就像虽然有 Deno,但是我还是用 Node.js 一样。


所以,我就写这篇文章,说下我个人感觉 Typescript 的缺点、为何它的优点无法打动我用它替代 Javascript,以及跟推荐我使用 Typescript 的大家讲一下我不用 Typescript 的逻辑。


各位想骂我心里骂骂就好了,我今天过个生日也不容易。


缺陷


1. 语法丑陋,代码臃肿


我写两段相同的代码,大家感受下:


// js
const multiply = (i, j) => i * j

// ts
function multiply(i: number, j: number) {
return i + j
}

你看这类型注释,把好好的一段代码弄得这么乱……反正我看这样的 Typescript,花的反应时间一定比看上面的 Javascript 代码长。——不过也有可能是我比较熟悉 Javascript 吧。


复杂一点的东西也是一个道理(Apollo GraphQL 的代码):


// js
import React from 'react';
import ApolloClient from 'apollo-client';

let apolloContext;

export function getApolloContext() {
if (!apolloContext) {
apolloContext = React.createContext({});
}
return apolloContext;
}

export function resetApolloContext() {
apolloContext = React.createContext({});
}

铁定要比这个好得多:


// ts
import React from 'react';
import ApolloClient from 'apollo-client';

export interface ApolloContextValue {
client?: ApolloClient<object>;
renderPromises?: Record<any, any>;
}

let apolloContext: React.Context<ApolloContextValue>;

export function getApolloContext() {
if (!apolloContext) {
apolloContext = React.createContext<ApolloContextValue>({});
}
return apolloContext;
}

export function resetApolloContext() {
apolloContext = React.createContext<ApolloContextValue>({});
}

甚至有人提了个 issue 就是抱怨 Type 让它变得难用。


这么看,实在是为了这个假静态类型语言牺牲太多了,毕竟代码可读性还是很重要的。——之所以说它是假静态语言,是因为在真正的静态类型语言中,如 C 和 C++,不同的变量类型在内存中的存储方式不同,而在 Typescript 中不是这样。


比如,缺了这个可读性,debug 会变得更难。你是不是没有注意到我上面 multiply 的 Typescript 代码其实有 bug——应该是 * 而不是 +


2. 麻烦


浏览器不能直接执行 Typescript,所以 Typescript 必须要被编译成 Javascript 才能执行,要花一段时间;项目越大,花的时间越长,所以 Deno 才要停用它。并且,使用 Typescript 要安装新的依赖,虽然的确不麻烦,但是不用 Typescript,就不用再多装一个依赖了是不是。


其实还有一点,但是放不上台面来讲,因为这是我自己的问题。


我一直不大喜欢给添花样的东西,比如 pugtypescriptdeno 等;虽然 scss 啥的我觉得还是不错的——没有它我就写不出 @knowscount/vue-lib 这个仓库。


3. 文件体积会变大


随随便便就能猜到,我写那么多额外的类型注释、代码变得那么臃肿肯定会让 Typescript 文件比用 Javascript 编写的文件更大。作为一个用 “tab 会让文件体积更小” 作为论据的 tab 党,我当然讨厌 Typescript 啦哈哈哈哈。


我理解在编译过后都是一样的,但是反正……我还是不爽。而且正是由于 TypeScript 会被编译到JavaScript 中,所以才会出现无论你的类型设计得多么仔细,还是有可能让不同的值类型潜入 JavaScript 变量中的问题。这是不可避免的,因为 JavaScript 仍然是没有类型的。


4. 报错使我老花


单纯吐槽一句,为什么它的报错那么丑,我就拼错了一个单词他给我又臭又长报一大段,还没有颜色。。


为何无法打动我


在讲为什么 Typescript 的优点无法打动我之前,我先来讲一讲 Typescript 有哪些优点吧:



  1. 大厂的产品

  2. 大厂在用

  3. 可以用未来的特性

  4. 降低出 bug 的可能性

  5. 面对对象编程(OOP)


对于它是微软的产品,我不能多说啥,毕竟我用 Visual Studio Code 用得很香;但是大厂在用这个论点,就不一样了。


有个逻辑谬误叫做「reductio ad absurdum」,也就是「归谬法」。什么意思呢:



大厂用 Typescript,所以我要用 Typescript。

大厂几百万改个 logo,我就借几百万改个 logo,因为大厂是大厂,肯定做得对。



这就很荒谬。


的确,大公司会采用 Typescript,必定有他的道理。但是,同样的论证思路也可以用于 FlowAngularVueEmberjQueryBootstrap 等等等等,几乎所有流行的库都是如此,那么它们一定都适合你吗?


关于它可以让你提前接触到未来的特性……大哥,babel 不香吗?


最后就是 OOP 以及降低出 bug 的可能性(Typesafe)。OOP 是 Typescript 的核心部分,而现在 OOP 已经不吃香了……例如 Ilya Suzdalnitski 就说过它是「万亿美元的灾难」。


v2-9cc25cb80ddc2df7ab06f0184e433790_1440w.jpg


至于为什么这么说,无非就两点——面向对象代码难以重构,也难以进行单元测试。重构时的抓狂不提,单元测试的重要性,大家都清楚吧。


而在 Javascript 这种非 OOP 语言里头,函数可以独立于对象存在。不用为了包含这些函数而去发明一些奇怪的概念真是一种解脱。


总之,TypeScript 的所谓优点(更好的错误处理、类型推理)都不是最佳方案。你还是得写测试,还是得好好命名你的函数和变量。个人觉得单单像 Typescript 一样添加一个接口或类型不能解决任何这些问题。


正好一千五百字。


作者:TurpinHero
链接:https://juejin.cn/post/6961012856573657095

收起阅读 »

我是如何把vue项目启动时间从70s优化到7秒的

可怕的启动时间 公司的产品是一个比较大的后台管理系统,而且使用的是webpack3的vue模板项目,单次项目启动时间达到了70s左右 启动个项目都够吃一碗豆腐脑了,可是没有豆腐脑怎么办,那就优化启动时间吧! 考虑到升级webpack版本的风险还是比较大的,出...
继续阅读 »

可怕的启动时间


公司的产品是一个比较大的后台管理系统,而且使用的是webpack3的vue模板项目,单次项目启动时间达到了70s左右


image.png


启动个项目都够吃一碗豆腐脑了,可是没有豆腐脑怎么办,那就优化启动时间吧!


考虑到升级webpack版本的风险还是比较大的,出了一点问题都得找我,想想还是先别冒险,稳妥为主,所以我选择了通过插件来优化构建时间。


通过查阅资料,提升webpack的构建时间有以下几个方向:



  • 多进程处理文件,同一时间处理多个文件

  • 预编译资源模块,比如把长时间不变的库提取出来做预编译,构建的时候直接取编译结果就好

  • 缓存,未修改的模块直接拿到处理结果,不必编译

  • 减少构建搜索和处理的文件数量


针对以上几种优化方向,给出以下几种优化方案。


多进程构建


happypack


happypack 的作用就是将文件解析任务分解成多个子进程并发执行。


子进程处理完任务后再将结果发送给主进程。所以可以大大提升 Webpack 的项目构件速度。


查看happypack的github,发现作者已经不再维护该插件,并且作者推荐使用webpack官方的多进程插件thread-loader,所以我放弃了happypacy,选择了thread-loader。


thread-loader


thread-loader是官方维护的多进程loader,功能类似于happypack,也是通过开启子任务来并行解析文件,从而提高构建速度。


把这个loader放在其他loader前面。不过该loader是有限制的。示例:



  • loader无法发出文件。

  • loader不能使用自定义加载器API。

  • loader无法访问网页包选项。


每个worker都是一个单独的node.js进程,其开销约为600毫秒。还有进程间通信的开销。在小型项目中使用thread-loader可能并不能优化项目的构建速度,反而会拖慢构建速度,所以使用该loader时需要明确项目构建构成中真正耗时的过程。


我的项目中我主要是用该loader用来解析vue和js文件,作用于vue-loaderbabel-loader,如下代码:


const threadLoader = {
loader: 'thread-loader',
options: {
workers: require('os').cpus().length - 1,
}
}

module.exports = {
module:{
rules: [
{
test: /\.vue$/,
use: [
threadLoader, // vue-loader前使用该loader
{
loader: 'vue-loader',
options: vueLoaderConfig
}
],
},
{
test: /\.js$/,
use: [
threadLoader, // babel-loader前使用该loader
{
loader: 'babel-loader',
options: {
cacheDirectory: true
}
}
]
}
]
}
}


配置了thread-loader后,重新构建试试,如下图所示,大概缩短了10秒的构建时间,还不错。


image.png


利用缓存提升二次构建的速度


虽然使用了多进程构建项目使构建时间缩短了10秒,但是一分钟的构建时间依然让人无法接受,这种挤牙膏似的优化方式多少让人有点不爽,有没有比较爽的方法来进一步缩短构建时间呢?


答案是有的,使用缓存。


缓存,不难理解就是第一次构建的时候将构建的结果缓存起来,当第二构建时,查看对应缓存是否修改,如果没有修改,直接使用缓存,由此,我们可以想象,当项目的变化不大时,大部分缓存都是可复用的,拿构建的速度岂不是会有质的飞跃。


cache-loader


说到缓存,当然百度一查,最先出现的就是cache-loader,github搜索下官方文档,得到如下结果:


该loader会缓存其他loader的处理结果,把该loader放到其他loader的前面,同时该loader保存和读取缓存文件也会有开销,所以建议在开销较大的loader前使用该loader。


文档很简单,考虑到项目中的vue-loaderbabel-loadercss-loader会有比较大的开销,所以为这些loader加上缓存,那么接下来就把cache-loader加到项目中吧:


const cacheLoader = {
loader: 'cache-loader'
}

const threadLoader = {
loader: 'thread-loader',
options: {
workers: require('os').cpus().length - 1,
}
}

module.exports = {
module:{
rules: [
{
test: /\.vue$/,
use: [
cacheLoader,
threadLoader, // vue-loader前使用该loader
{
loader: 'vue-loader',
options: vueLoaderConfig
}
],
},
{
test: /\.js$/,
use: [
cacheLoader,
threadLoader, // babel-loader前使用该loader
{
loader: 'babel-loader',
options: {
cacheDirectory: true
}
}
]
}
]
}
}


util.js文件中,该文件主要是生成css相关的webpack配置,找到generateLoaders函数,修改如下:


  const cacheLoader = {
loader: 'cache-loader'
}

function generateLoaders(loader, loaderOptions) {
// 在css-loader前增加cache-loader
const loaders = options.usePostCSS ? [cacheLoader, cssLoader, postcssLoader] : [cacheLoader, cssLoader]

if (loader) {
loaders.push({
loader: loader + '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}

// Extract CSS when that option is specified
// (which is the case during production build)
if (options.extract) {
return ExtractTextPlugin.extract({
use: loaders,
fallback: 'vue-style-loader',
// 添加这句配置解决element-ui的图标路径问题
publicPath: '../../'
})
} else {
return ['vue-style-loader'].concat(loaders)
}
}

如上配置完成后,再次启动项目,可以发现,现在的启动时间没什么变化,然后我们二次启动项目,可以发现现在的启动时间来到了30s左右,前面我们已经说过了,cache-loader缓存只有在二次启动的时候才会生效。


image.png


虽然项目启动时间优化了一半还多,但是我们的欲望是无限大的,30秒的时间离我们的预期还是有点差距的,继续优化!


hard-source-webpack-plugin


HardSourceWebpackPlugin是一个webpack插件,为模块提供中间缓存步骤。为了查看结果,您需要使用此插件运行webpack两次:第一次构建将花费正常的时间。第二次建设将大大加快。


话不多说,直接配置到项目中:


const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
module.exports = {
//...
plugins: [
new HardSourceWebpackPlugin()
]
}

image.png


二次构建时,我们会发现构建时间来到了个位数,只有短短的7秒钟。


在二次构建中,我发现了一个现象,构建的进度会从10% 一下跳到 80%,甚至是一瞬间就完成了中间构建过程。这正验证了该插件为模块提供中间缓存的说法。


为模块提供中间缓存,我的理解是cache-loader缓存的是对应loader的处理结果 ,而这个插件甚至可以缓存整个项目全部的处理结果,直接引用最终输出的缓存文件,从而大大提高构建速度。


其他优化方法


babel-loader开启缓存


babel-loader自带缓存功能,开启cacheDirectory配置项即可,官网的说法是,开启缓存会提高大约两倍的转换时间。


module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [
...
{
loader: 'babel-loader',
options: {
cacheDirectory: true // 开启缓存
}
}
]
}
]
}
}

uglifyjs-webpack-plugin开启多进程压缩


uglifyjs-webpack-plugin或是其他的代码压缩工具都提供了多进程压缩代码的功能,开启可加速代码压缩。


动态polyfill


建议查看该篇文章


一文搞清楚前端 polyfill


总结


至此,我们完成了项目构建时间从70s到7s的优化过程,文中主要使用:





























一步步的将项目优化到几乎立马启动,哎,看来这下摸鱼的时间又少了,加油干吧,打工人!


作者:进击的小超人
链接:https://juejin.cn/post/6979879230297341989

收起阅读 »

桌面上的Flutter:Electron又多了个对手

从本质上看,Flutter 是一个独立的二进制可执行文件。它不仅改变了移动设备的玩法,在桌面设备上也同样不可小觑。一次编写,可在 Android、iOS、Windows、Mac 和 Linux 上进行原生部署,并通过 AngularDart 将所有的业务逻辑共...
继续阅读 »

从本质上看,Flutter 是一个独立的二进制可执行文件。它不仅改变了移动设备的玩法,在桌面设备上也同样不可小觑。一次编写,可在 Android、iOS、Windows、Mac 和 Linux 上进行原生部署,并通过 AngularDart 将所有的业务逻辑共享到 Web 上,这也是它的一大特点。


原生桌面客户端加速移动开发

在进入真实的原生桌面应用程序之前,先让我们看看在桌面上运行的 Flutter 可以为开发移动设备的人们带来哪些好处。


 启动时间


首先是启动 Android 模拟器和运行 Gradle。


下面的动图记录了模拟器冷启动并运行默认的 Flutter 应用程序。我只截取了其中的 2 分 40 秒,可以看出来在那段时间内可以发生很多事情。



但如果我告诉你,你可以在不到 10 秒的时间内启动并运行应用程序,你会怎么想?


运行原生应用程序可以省去启动 Android 模拟器和运行 Gradle 的全部开销。


看看这个:



请注意,你不必离开 IntelliJ。我们开发了将 Flutter 作为原生应用程序所需的工具,它适用于所有的 Flutter IDE。


 在运行时调整大小

与其他应用程序一样,你需要测试不同大小的布局,那么你需要做些什么?


你要求你的朋友使用不同的手机或者创建一组模拟器,以确保你的布局在每台设备上都是正常的。


这对我来说是个麻烦事。我们能更简单一点吗?


可以!



 使用 PC 上的资源

在开发和测试需要与手机上的资源发生交互的应用程序时,首先需要将所有测试文件移动到模拟器或设备上,这样可能会非常烦人。


如果只需要使用原生文件选择器来选择你想要的文件会不会更好?



 热重载和调试

热重载和调试功能是每个高效率工程师所必须的。



 内存占用

对于使用笔记本电脑或配置不太好的电脑的人来说,内存是非常重要的。


Android 模拟器占用大约 1GB 的内存。现在想象一下,为了测试一个聊天应用程序或类似的程序,需要启动 IntelliJ 和狂吃内存的 Chrome。



因为嵌入器是以原生的方式运行,所以不需要 Android 模拟器。这使它的内存占用变得更小。



原生桌面应用

只是在桌面上运行一个 Flutter 应用程序对于可立即发布的成熟桌面应用程序来说是远远不够的。这样做感觉上就像在桌面上运行移动应用程序。


少了什么东西?很多!


悬停、光标变化、滚轮交互,等等。


我们设法在不改变任何平台代码的情况下实现这些功能——它是一个独立的软件包,可以被包含在任何普通的 Flutter 应用程序中。但是,当与桌面嵌入器一起使用时,奇迹就发生了!



这是在 Android 模拟器运行完全相同的代码的结果。



同时开发 Android 和桌面应用程序。



桌面小部件展示

悬停:



光标:



开发一个真正的跨平台应用——包括桌面
 小部件

你创建的大多数小部件都是普遍可用的,如按钮、加载指标器等。


那些需要根据平台呈现不同外观的小部件可以通过 TargetPlatform 属性进行封装,非常容易。


像 CursorWidget 这样的小部件也可以被包含在 Android 版本中。


 页面

根据平台和屏幕尺寸的不同,页面也会有很大差异。不过它们大多只是布局不同,而不是功能差异。


使用 PageLayoutWidget 可以轻松地为每个平台创建准确的布局。



默认情况下对平板电脑也提供了很好的支持。


 插件

使用同时支持桌面嵌入器的插件时,不需要修改 Flutter 代码。


 代码什么时候发布?

很快。不过这个项目仍然处于测试阶段,在不久的将来很可能会发生一些变化。


我们的目标是在不久的将来发布易于安装、设置和使用的产品。


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

高效开发:分享 `extension` 有趣的用法

前言 extension 可以在不更改类或创建子类的情况下,向类添加扩展功能的一种方式。灵活使用 extension 对基础类进行扩展,对开发效率有显著提升。 举个栗子🌰,对 int 类型扩展 小轰在开发项目中碰到需求:将单位为分的数值转换成单位为元的字符串 ...
继续阅读 »

前言


extension 可以在不更改类或创建子类的情况下,向类添加扩展功能的一种方式。灵活使用 extension 对基础类进行扩展,对开发效率有显著提升。


举个栗子🌰,对 int 类型扩展


小轰在开发项目中碰到需求:将单位为分的数值转换成单位为元的字符串


/// 通常的写法,封装转换方法

///封装方法:金额转字符串 保留两位小数
String convertPointToUnit(int num){
return (num.toDouble() / 100).toStringAsFixed(2);
}

///使用
void main(){
int num = 100;
var result = convertPointToUnit(num);
print(result); //打印结果为 1.00
}

同样的功能,使用 extension 进行开发,会更加简洁,如下:


/// 使用 extension 对 int 类进行扩展,添加方法 moneyString
extension ExInt on int {
/// 金额转字符串 保留两位小数
/// 100 => 1.00
String get moneyString => (this.toDouble() / 100).toStringAsFixed(2);
}

import ../ExInt.dart;
///使用
void main(){
int num = 100;
print(num.moneyString);
}

扩展后,直接作为该类型的成员方法来被使用。extension 就像是基因赋值,直接将能力(方法)对宿主进行赠与。


各种场景的扩展演示



  • 对枚举进行扩展实现


enum FruitEnum { apple, banana }

extension ExFruitEnum on FruitEnum {
String get name {
switch (this) {
case FruitEnum.apple:
return "apple";
case FruitEnum.banana:
return "banana";
}
}
}

///字符串匹配枚举
FruitEnum generateFruit (String fruitType){
if(fruitType == FruitEnum.apple.name){
return FruitEnum.apple;
} else if(fruitType == FruitEnum.banana.name){
return FruitEnum.banana;
}
}


  • 扩展作用于泛型:


//扩展list的方法
extension ExList<T> on List<T> {
//扩展操作符
List<T> operator -() => reversed.toList();
//一个链表分割成两个
List<List<T>> split(int at) => <List<T>>[sublist(0, at), sublist(at)];
}


  • 扩展在 Widget 控件中的应用


我们会有类似的控件


Column(
children: <Widget>[
Container(
paddint: const EdgeInsets.all(10)
child: AWidget(),
),
Container(
paddint: const EdgeInsets.all(10)
child: BWidget(),
),
Container(
paddint: const EdgeInsets.all(10)
child: CWidget(),
),
]
)

代码中有很多的冗余对吧?我们用 extension 进行扩展一下:


extension ExWidget on Widget {
Widget paddingAll(double padding) {
return Container(
paddint: const EdgeInsets.all(padding)
child: this,
);
}
}

之后我们就可以改成:


Column(
children: <Widget>[
AWidget().paddingAll(10),
BWidget().paddingAll(10),
CWidget().paddingAll(10),
]
)

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

Android程序员如何从设计角度思考HTTPS

typora-root-url: img typora-copy-images-to: img 从设计角度思考HTTPS 我们了解了HTTP协议的内容后,明白HTTP存在很多安全隐患,所以后来推出了安全协议-HTTPS,我们不妨站在设计角度来设计一个安全的HT...
继续阅读 »
typora-root-url: img
typora-copy-images-to: img

从设计角度思考HTTPS


我们了解了HTTP协议的内容后,明白HTTP存在很多安全隐患,所以后来推出了安全协议-HTTPS,我们不妨站在设计角度来设计一个安全的HTTP连接协议,看看HTTP存在哪些问题,我们该如何设计能保证安全性,从而了解HTTPS的安全协议是如何保障的HTTP安全

首先我们需要考虑一下,实现HTTPS所谓的安全,我们需要保证那些地方的安全:


1.首先我们需要保证服务端和客户端之间发送的消息是安全的


2.其次我们要保证服务端和客户端之间的连接是安全的


3.最后我们还要保证服务端不会被其他的伪造客户端连接,并且通过此方式破解加密方式


服务端/客户端信息交互的安全


首先我们先来考虑一下,有什么方法可以保证客户端发送的消息给服务端并且服务端返回结果,这个过程是安全的,大概的过程如下:


image.png


这个时候我们最先想到的方案--加密,我们使用加密算法给数据加密了不就行了吗,那么该选择什么加密算法呢?开发过程中最常见的加密算法如:MD5、SHA1这样的摘要算法或者aes、des这样的对称加密算法,又或者rsa这样的非对称算法,看起来每一种都可以实现数据加密传输,但是我们不要忘记了,第三点中我们希望能保证其他客户端连接不会破解此种加密方式,要知道在互联网中,不是仅仅一台客户端和一个服务端交互,可以有无数台客户端同时与服务端交互,这个时候我们如果要防止别的客户端破解加密,看起来摘要算法这种不可逆的算法刚好合适,但是我们不要忘记了,客户端和服务端需要完成交互的,那么也就是说这个加密不能为不可逆算法,否则客户端也无法对服务端数据进行处理,服务端也无法处理客户端数据,那么只能是对称加密算法或者非对称加密算法能满足了,我们继续思考,如果是多台客户端同时连接服务端,如下图:


image.png


那么似乎哪一种加密都能满足,那么我们不禁有个问题,万一有黑客(恶意机器)拦截了我们的请求,并且充当了中间的传输者,我们的这两种加密算法还安全吗?如下图:


image.png


可以看到,我们的客户端和服务端中间被未知的恶意机器拦截转发了请求,那么我们之前的加密方式如果是直接传递的加密方式和密钥,如果是对称加密那么结局可想而知,对于中间机器来说,依然可以解密出客户端和服务端的消息,对于黑客来说依然是透明的,安全性仅仅比不加密强上一点点,完全不可以称之为可信任的安全协议,那么使用非对称加密呢?我们都知道非对称加密是一堆密钥,每一端持有自己的私钥,对外公开公钥,而公钥加密仅仅使用私钥才可以解密,这样即使有中间机器拦截,也仅仅能拿到客户端和服务端的公钥,但是我们不要忘记了,客户端应该是持有服务端的公钥,用公钥加密传输给服务端,服务端私钥解密,响应的过程即是客户端的私钥解密服务端持有的客户端公钥,中间机器即使拦截了双方的公钥,也无法解密双方公钥自身加密的信息,这样的话,客户端和服务端数据传输安全的问题似乎完美解决了


新隐患-公钥传输方式


刚刚我们经过对比,确定了使用公私钥方式的非对称加密来作为客户端-服务端传输的加密方式,看起来应该高枕无忧了,那么事实真的如此吗?其实和对称加密一样,非对称加密这样直接传输加密,也仅仅是提高了一点点安全性而已,如果遇到的黑客在拦截到客户端的请求后,将自身的公钥传递给服务端以及客户端,而将客户端/服务端的公钥持有会如何?是的,细极思恐,那样中间机器将拥有解密双端消息的能力!为什么会这样?试想一下,客户端使用所谓服务端的公钥加密消息,发送,被中间机器拦截后,这所谓的服务端公钥是中间机器的,那么私钥岂不是可以解密拿到明文信息?然后再伪装使用拦截到的真实的客户端的公钥加密,转发给服务端,同理,服务端的所谓客户端公钥加密对于中间机器完全形同虚设,那么这种问题如何解决呢?我们可不可以更换一种公钥传输方式,尽量绕开中间机器的拦截,保证安全性呢?


我们可以想下,大概有如下两种方法传输公钥:


1.服务端把公钥发送给每一个连接进来的客户端


2.将公钥放到一个地方(比如独立的服务器,或者文件系统),客户端需要获取公钥的时候,访问这个地方的公钥来和服务端进行匹配


而第一个方案,即我们刚刚推翻的方案,很明显会存在被拦截调包的可能,那么似乎我们只能使用第二个方案来传输公钥?那么我们不禁有个问题,即客户端是如何知道存放公钥的远程服务器地址以及认证加密方式,而且每次建立连接都要来获取一次,对服务器的抗压能力也有一定的考验?还有如何保证黑客等恶意访问的用户不能通过此种方式拿到公钥,所以安全也是个比较麻烦的问题


引入第三方CA机构


由于上述提到的问题,所以对于个人而言,如果在开发网站的同时,还要再花费大量金钱和精力在开发公钥服务上,是很不合理的,那么有木有专门做这个的公司,我们托管给这个公司帮我们完成,只需要付出金钱的代价就能体验到服务不可以吗?于是,专门负责证书认证的第三方CA机构出现了,我们只需要提前申请好对应的服务端信息,并且提交对应资料,付出报酬,CA就会给我们提供对应的服务端认证服务,大大减少我们的操作和复杂度,但是这个时候我们不禁又有个问题,CA机构能保证只有客户端拿到认证的证书,并且认证通过,拦截对应的非正常客户端吗?如果不能的话,那岂不是黑客也可以拿到认证?现在的问题开始朝着如何认证用户真伪方向发展了


验证证书有效性


其实想要解决认证的问题,我们可以从生活中寻找一些灵感,我们每个人都有一个唯一的id,证明身份,这样可以保证识别出id和对应的人,也能识别不法分子,那么,既然计算机来源于生活,设计出来的东西也应该遵循正常的逻辑,我们何不给每个证书设置类似id的唯一编号呢?当然计算机是死的,没办法简单的将机器和证书编号进行绑定,那么就需要设计一个符合逻辑的证书验证过程。我们不妨思考下,平时开发的软件为了识别被人篡改的软件,我们是如何做的,相信大脑里有个词会一闪而过,MD5/SHA1(签名)?没错,那么我们证书的认证可否按照这个思路设计?


现在我们假设,客户端拿到证书后,能够从证书上拿到公钥信息、证书签名hash和有效期等信息,也就是说证书内置了计算整个证书的签名hash值,如果此时我们根据客户端的签名算法进行一次加签计算,和证书默认计算好的hash比较,发现不一致,那么就说明证书被修改了,肯定不是第三方发布的正式证书,如果一致,说明证书是真实的,没有被篡改,我们可以尝试与服务端连接了,因为证书拿到了,也有了公钥,后续的就是加密通信的过程了


至此,似乎一个安全的加密https简陋的设计出来了,也似乎解决了这些安全问题,但是不得不提的一点是,我们上面有个很重要的一点,即存放证书的服务器一定要保证安全性,第三方机构算不算绝对安全呢?答案是否定的,因为在https至今的历史上,发生过第三方机构被黑客攻击成功,黑客使用的也是正版的证书的事件,只能说计算机的世界不存在绝对安全,而是相对来说,安全系数提高了太多


HTTPS认证过程


前面我们设计了简陋版的HTTPS,那么,我们接下来看看,正版的HTTPS大体认证过程是如何的,首先我们从申请证书开始:


image.png


可以看到,申请证书的时候,需要提供很多内容,其中域名、签名hash算法、加密算法是最重要的,通过这三项计算生成证书以及确定加密认证算法,并且在这个过程中还需要提供服务端自己的公钥,用来生成证书,CA机构使用自己的私钥加密证书,生成证书传递给服务端


2.证书申请拿到以后,客户端tcp三次握手连接(会携带一个随机数client-random),这个时候服务端将证书信息(包含过期时间、签名算法、当前证书的hash签名、服务端公钥、颁发证书机构等信息)传递给客户端,并且传递一个random随机数


3.客户端收到证书后,使用浏览器内置的CA认证,对证书的颁发机构逐个/逐层校验,确定证书来源正常,并且校验证书过期时间,确定是否可用,最后根据证书的签名算法,计算出对应的签名hash,和证书内置的签名hash比较,确定是否是未篡改的证书,完全认证通过后,证书认证环节结束


4.客户端生成随机对称密钥( pre-master ),将双端随机数组合通过证书的公钥(服务端的公钥)加密后,发送给服务端,服务端收到后,根据双方生成的随机数组合验证击进行http通信


以上就是HTTPS认证的大体流程,另外需要注意的是,HTTPS使用了签名算法(MD5/SHA256等)、对称加密以及非对称加密完成了整个交互过程,在认证过程中仅仅使用了签名算法和非对称加密保证建立通道的安全稳定,在通道建立过程中,维持了一个sessionid,用来防止频繁创建通道大量消耗资源,尽可能保证通道长期连接复用,并且我们需要知道一点,非对称加密虽然安全,但是相比较对称加密,加密解密步骤复杂导致时间会更久,所以HTTPS在建立通道以后,会选择双端协议使用对称加密来完成后续的数据交互,而上述提到的双方的随机对称密钥组合是用来在建立连接后的第一次交互的过程中,二次确认握手过程是否被篡改(客户端把Client.random + sever.random + pre-master组合后使用公钥加密,并且把握手消息根据证书的签名算法计算hash,发送给服务端确认握手过程是否被窜改),完成校验后,确定当前是安全连接后,双端之间就会使用约定好的对称加密算法进行数据加密解密传输,至此一个完整的HTTPS协议完成


相关视频


Android程序员中高级进阶学习/OkHttp原理分析


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

做一个短链接系统需要考虑这么多

什么是短链接短链接顾名思义,就是一个比较短的链接(我好像说了个废话),我们平时看到的链接可能长这样:mp.weixin.qq.com/s?biz=MzU5M…又臭又长有没有(没错,这是我的WX公众号链接,可以关注一下),那如果我们需要将某个链接发在某个文章或者...
继续阅读 »



什么是短链接

短链接顾名思义,就是一个比较短的链接(我好像说了个废话),我们平时看到的链接可能长这样:

mp.weixin.qq.com/s?biz=MzU5M…

又臭又长有没有(没错,这是我的WX公众号链接,可以关注一下),那如果我们需要将某个链接发在某个文章或者推广给别人的时候,这么长看着也太不爽了,而短链接的出现就是用一个很短的URL来替代这个很长的家伙,当用户访问短链接的时候,会重定向到原来的链接。比如长下面这样:

sourl.cn/CsTkky

你如果平时有注意的话,各种商业短信上的链接也是会转成特别短的:

img

这个特别短的URL就是短链接。

为什么需要URL短链接

URL短链接用于为长URL创建较短的别名,我们称这些缩短的别名为“短链接”;当用户点击这些短链接时,会被重定向到原始URL;短链接在显示、打印、发送消息时可节省大量空间。

例如,如果我们通过sourl缩短以下URL:

juejin.cn/user/211951…

我们可以得到一个短链接:

sourl.cn/R99fbj

缩短的URL几乎是实际URL大小的三分之一。

URL缩写经常用于优化设备之间的链接,跟踪单个链接以分析受众,衡量广告活动的表现,或隐藏关联的原始URL。

如果你以前没有使用过sourl,可以尝试创建一个新的URL短链接,并花一些时间浏览一下他们的服务提供的各种选项。可以让你更好的理解这篇文章。

系统的要求和目标

在完成一个功能或者开发一个系统时,先确定系统的定位和要达到的目标是一个好的习惯,这可以让你在设计和开发过程中有更清晰的思路。

我们的短链接系统应满足以下要求:

功能要求:

  • 给定一个URL,我们的服务应该为其生成一个较短且唯一的别名,这叫做短链接,此链接应足够短,以便于复制和粘贴到应用程序中;

  • 当用户访问短链接时,我们的服务应该将他们重定向到原始链接;

  • 用户应该能够选择性地为他们的URL选择一个自定义的短链接;

  • 链接可以在指定时间跨度之后过期,用户应该能够指定过期时间。

非功能要求:

  • 系统必须高度可用。如果我们的服务关闭,所有URL重定向都将开始失败。

  • URL重定向的延迟情况应该足够小;

  • 短链接应该是不可猜测的。

扩展要求:

  • 支持分析和统计,例如短链接的访问次数;

  • 其他服务也应该可以通过RESTAPI访问我们的服务。

容量要求和限制

我们的系统将会有很大的访问量。会有对短链接的读取请求和创建短链接的写入请求。假设读写比例为100:1。

访问量预估:

假设我们每个月有5亿个新增短链接,读写比为100:1,我们可以预计在同一时间内有500亿重定向:

100 * 5亿 => 500亿

我们系统的QPS(每秒查询数量)是多少?每秒的新短链接为:

5亿/ (30天 * 24小时 * 3600 秒) ≈ 200 URLs/s

考虑到100:1读写比,每秒URL重定向将为:

100 * 200 URLs/s = 20000/s

存储预估:

假设我们将每个URL缩短请求(以及相关的缩短链接)存储5年。由于我们预计每个月将有5亿个新URL,因此我们预计存储的对象总数将为300亿:

5亿 * 5 年 * 12 月 = 300亿 

假设每个存储的对象大约有500个字节(这只是一个估算值)。我们将需要15TB的总存储:

300亿*500bytes≈15TB

带宽预估:

对于写请求,由于我们预计每秒有200个新的短链接创建,因此我们服务的总传入数据为每秒100KB:

200*500bytes≈100KB/s

对于读请求,预计每秒约有20,000个URL重定向,因此我们服务的总传出数据将为每秒10MB:

20000 * 500 bytes ≈10 MB/s

内存预估:

对于一些热门访问的URL为了提高访问速率,我们需要进行缓存,需要多少内存来存储它们?如果我们遵循二八原则,即20%的URL产生80%的流量,我们希望缓存这20%的热门URL。

由于我们每秒有20,000个请求,因此我们每天将收到17亿个请求:

20000 * 24 * 3600 ≈ 17亿

要缓存这些请求中的20%,我们需要170 GB的内存:

17亿 * 0.2 * 500bytes ≈ 170GB

这里需要注意的一件事是,由于将会有许多来自相同URL的重复请求,因此我们的实际内存使用量可能达不到170 GB。

整体来说,假设每月新增5亿个URL,读写比为100:1,我们的预估数据大概是下面这样:

类型预估数值
新增短链接200/s
短链接重定向20000/s
传入数据100KB/s
传出数据10 MB/s
存储5年容量15 TB
内存缓存容量170 GB

系统API设计

一旦我们最终确定了需求,就可以定义系统的API了,这里则是要明确定义我们的系统能提供什么服务。

我们可以使用REST API来公开我们服务的功能。以下是用于创建和删除URL的API的定义:

创建短链接接口

String createURL(api_dev_key, original_url, custom_alias=None, user_name=None, expire_date=None)
复制代码

参数列表:

api_dev_key:分配给注册用户的开发者密钥,可以根据该值对用户的创建短链接数量进行限制;

original_url:需要生成短链接的原始URL;

custom_alias :用户对于URL自定义的名称;

user_name :可以用在编码中的用户名;

expire_date :短链接的过期时间;

返回值:

成功生成短链接将返回短链接URL;否则,将返回错误代码。

删除短链接接口

String deleteURL(api_dev_key, url_key)
复制代码

其中url_key是表示要删除的短链接字符串;成功删除将返回delete success

如何发现和防止短链接被滥用?

恶意用户可以通过使用当前设计中的所有URL密钥来对我们进行攻击。为了防止滥用,我们可以通过用户的api_dev_key来限制用户。每个api_dev_key可以限制为每段时间创建一定数量的URL和重定向(可以根据开发者密钥设置不同的持续时间)。

数据模型设计

在开发之前完成数据模型的设计将有助于理解各个组件之间的数据流。

在我们短链接服务系统中的数据,存在以下特点:

  • 需要存储十亿条数据记录;

  • 存储的每个对象都很小(小于1K);

  • 除了存储哪个用户创建了URL之外,记录之间没有任何关系;

  • 我们的服务会有大量的读取请求。

我们需要创建两张表,一张用于存储短链接数据,一张用于存储用户数据;

img

应该使用怎样的数据库?

因为我们预计要存储数十亿行,并且不需要使用对象之间的关系-所以像mongoDB、Cassandra这样的NoSQL存储是更好的选择。选择NoSQL也更容易扩展。

基本系统设计与算法

现在需要解决的问题是如何为给定的URL生成一个简短且唯一的密钥。主要有两种解决方案:

  • 对原URL进行编码

  • 提前离线生成秘钥

对原URL编码

可以计算给定URL的唯一HASH值(例如,MD5或SHA256等)。然后可以对HASH进行编码以供显示。该编码可以是base36([a-z,0-9])base62([A-Z,a-z,0-9]),如果我们加上+/,就可以使用Base64编码。需要考虑的一个问题是短链接的长度应该是多少?6个、8个或10个字符?

使用Base64编码,6个字母的长密钥将产生64^6≈687亿个可能的字符串; 使用Base64编码,8个字母长的密钥将产生64^8≈281万亿个可能的字符串。

按照我们预估的数据,687亿对于我们来说足够了,所以可以选择6个字母。

如果我们使用MD5算法作为我们的HASH函数,它将产生一个128位的HASH值。在Base64编码之后,我们将得到一个超过21个字符的字符串(因为每个Base64字符编码6位HASH值)。

现在我们每个短链接只有6(或8)个字符的空间,那么我们将如何选择我们的密钥呢?

我们可以取前6(或8)个字母作为密钥,但是这样导致链接重复;要解决这个问题,我们可以从编码字符串中选择一些其他字符或交换一些字符。

我们的解决方案有以下问题:

解决办法:

我们可以将递增的序列号附加到每个输入URL以使其唯一,然后生成其散列。不过,我们不需要将此序列号存储在数据库中。此方法可能存在的问题是序列号不断增加会导致溢出。添加递增的序列号也会影响服务的性能。

另一种解决方案可以是将用户ID附加到输入URL。但是,如果用户尚未登录,我们将不得不要求用户选择一个唯一的key。即使这样也有可能有冲突,需要不断生成直到得到唯一的密钥。

离线生成秘钥

可以有一个独立的密钥生成服务,我们就叫它KGS(Key Generation Service),它预先生成随机的六个字母的字符串,并将它们存储在数据库中。每当我们想要生成短链接时,都去KGS获取一个已经生成的密钥并使用。这种方法更简单快捷。我们不仅不需要对URL进行编码,而且也不必担心重复或冲突。KGS将确保插入到数据库中的所有密钥都是唯一的。

会存在并发问题吗?

密钥一旦使用,就应该在数据库中进行标记,以确保不会再次使用。如果有多个服务器同时读取密钥,我们可能会遇到两个或多个服务器尝试从数据库读取相同密钥的情况。如何解决这个并发问题呢?

KGS可以使用两个表来存储密钥:一个用于尚未使用的密钥,一个用于所有已使用的密钥。

一旦KGS将密钥提供给其中一个服务器,它就可以将它们移动到已使用的秘钥表中;可以始终在内存中保留一些密钥,以便在服务器需要时快速提供它们。

为简单起见,一旦KGS将一些密钥加载到内存中,它就可以将它们移动到Used Key表中。这可确保每台服务器都获得唯一的密钥。

如果在将所有加载的密钥分配给某个服务器之前KGS重启或死亡,我们将浪费这些密钥,考虑到我们拥有的秘钥很多,这种情况也可以接受。

还必须确保KGS不将相同的密钥提供给多个服务器,因此,KGS将秘钥加载到内存和将秘钥移动到已使用表的动作需要时同步的,或者加锁,然后才能将秘钥提供给服务器。

KGS是否存在单点故障?

要解决KGS单点故障问题,我们可以使用KGS的备用副本。当主服务器死机时,备用服务器可以接管以生成和提供密钥。

每个应用服务器是否可以换成一些Key?

可以,这样可以减少对KGS的访问,不过,在这种情况下,如果应用服务器在使用所有密钥之前死亡,我们最终将丢失这些密钥。但是因为我们的秘钥数量很多,这点可以接受。

如何完成秘钥查找?

我们可以在数据库中查找密钥以获得完整的URL。如果它存在于数据库中,则向浏览器发回一个“HTTP302 Redirect”状态,将存储的URL传递到请求的Location字段中。如果密钥不在我们系统中,则发出HTTP 404 Not Found状态或将用户重定向回主页。

数据分区和复制

因为我们要存储十亿个URL数据,那么一个数据库节点在存储上可能不满足要求,并且单节点也不能支撑我们读取的要求。

因此,我们需要开发一种分区方案,将数据划分并存储到不同的数据库服务中。

基于范围分区:

我们可以根据短链接的第一个字母将URL存储在不同的分区中。因此,我们将所有以字母'A/a'开头的URL保存在一个分区中,将以字母‘B/b’开头的URL保存在另一个分区中,以此类推。这种方法称为基于范围的分区。我们甚至可以将某些不太频繁出现的字母合并到一个数据库分区中。

基于hash值分区:

在此方案中,我们对要存储的对象进行Hash计算。然后,我们根据Hash结果计算使用哪个分区。在我们的例子中,我们可以使用短链接的Hash值来确定存储数据对象的分区。

Hash函数会将URL随机分配到不同的分区中(例如,Hash函数总是可以将任何‘键’映射到[1…256]之间的一个数字,这个数字将表示我们在其中存储对象的分区。

这种方式有可能导致有些分区数据超载,可以使用一致性哈希算法解决。

缓存

对于频繁访问的热点URL我们可以进行缓存。缓存的方案可以使用现成的解决方案,比如使用memcached,Redis等,因此,应用服务器在查找数据库之前可以快速检查高速缓存是否具有所需的URL。

如果确定缓存容量?

可以从每天20%的流量开始,并根据客户端的使用模式调整所需的缓存服务器数量。如上所述,我们需要170 GB内存来缓存20%的日常流量。可以使用几个较小的服务器来存储所有这些热门URL。

选择哪种淘汰策略?

淘汰策略是指当缓存已满时,如果我们想用更热点的URL替换链接,我们该如何选择?

对于我们的系统来说,最近最少使用(LRU)是一个合理的策略。在此策略下,我们首先丢弃最近最少使用的URL;我们可以使用一个短链接或短链接的HASH值作为key的Hash Map或类似的数据结构来存储URL和访问次数。

如何更新缓存?

每当出现缓存未命中时,我们的服务器都会命中后端数据库。每次发生这种情况,我们都可以更新缓存并将新条目传递给所有缓存副本。每个副本都可以通过添加新条目来更新其缓存。如果副本已经有该条目,它可以简单地忽略它。

负载均衡

可以在系统中的三个位置添加负载均衡层:

  • 在客户端和应用程序服务器之间;

  • 在应用程序服务器和数据库服务器之间;

  • 在应用程序服务器和缓存服务器之间。

可以使用简单的循环调度方法,在后端服务器之间平均分配传入的请求。这种负载均衡方式实现起来很简单,并且不会带来任何开销。此方法的另一个好处是,如果服务器死机,负载均衡可以让其退出轮换,并停止向其发送任何流量。

循环调度的一个问题是没有考虑服务器过载情况。因此,如果服务器过载或速度慢,不会停止向该服务器发送新请求。要处理此问题,可以放置一个更智能的解决方案,定期查询后端服务器的负载并基于此调整流量。

数据清除策略

数据应该永远保留,还是应该被清除?如果达到用户指定的过期时间,短链接应该如何处理?

  • 持续扫描数据库,清除过期数据。

  • 懒惰删除策略

如果我们选择持续查询过期链接来删除,将会给数据库带来很大的压力;可以慢慢删除过期的链接,并进行懒惰的方式清理。服务确保只有过期的链接将被删除,尽管一些过期的链接可以在数据库保存更长时间,但永远不会返回给用户。

  • 每当用户尝试访问过期链接时,我们都可以删除该链接并向用户返回错误;

  • 单独的清理服务可以定期运行,从存储和缓存中删除过期的链接;

  • 此服务应该非常轻量级,并计划仅在预期用户流量较低时运行;

  • 我们可以为每个短链接设置默认的到期时间(例如两年);

  • 删除过期链接后,我们可以将密钥放回KGS的数据库中重复使用。

结语

以上就是开发一个短链接服务系统要做的方方面面,可能还存在一些小黑没有考虑到的地方,欢迎留言区交流!如果对你有一点点帮助,点个赞鼓励一下。

作者:小黑说Java
来源:https://juejin.cn/post/7034325565431611406

收起阅读 »

Python编程需要遵循的一些规则v2

Python编程需要遵循的一些规则v2使用 pylintpylint 是一个在 Python 源代码中查找 bug 的工具. 对于 C 和 C++ 这样的强类型静态语言来说, 这些 bug 通常由编译器来捕获. 由于 Python 的动态特性, 有些警告可能不...
继续阅读 »



Python编程需要遵循的一些规则v2

使用 pylint

pylint 是一个在 Python 源代码中查找 bug 的工具. 对于 C 和 C++ 这样的强类型静态语言来说, 这些 bug 通常由编译器来捕获. 由于 Python 的动态特性, 有些警告可能不对. 不过虚报的情况应该比较少. 确保对你的代码运行 pylint. 在 CI 流程中加入 pylint 检查的步骤. 抑制不准确的警告, 以便其他正确的警告可以暴露出来。

自底向上编程

自底向上编程(bottom up): 从最底层,依赖最少的地方开始设计结构及编写代码, 再编写调用这些代码的逻辑, 自底向上构造程序.

  • 采取自底向上的设计方式会让代码更少以及开发过程更加敏捷.

  • 自底向上的设计更容易产生符合单一责任原则(SRP) 的代码.

  • 组件之间的调用关系清晰, 组件更易复用, 更易编写单元测试案例.

如:需要编写调用外部系统 API 获取数据来完成业务逻辑的代码.

  • 应该先编写一个独立的模块将调用外部系统 API 获取数据的接口封装在一些函数中, 然后再编写如何调用这些函数 来完成业务逻辑.

  • 不可以先写业务逻辑, 然后在需要调用外部 API 时再去实现相关代码, 这会产生调用 API 的代码直 接耦合在业务逻辑中的代码.

防御式编程

使用 assert 语句确保程序处于的正确状态 不要过度使用 assert, 应该只用于确保核心的部分.

注意 assert 不能代替运行时的异常, 不要忘记 assert 语句可能会被解析器忽略.

assert 语句通常可用于以下场景:

  • 确保公共类或者函数被正确地调用 例如一个公共函数可以处理 list 或 dict 类型参数, 在函数开头使用 assert isinstance(param, (list, dict))确保函数接受的参数是 list 或 dict

  • assert 用于确保不变量. 防止需求改变时引起代码行为的改变

if target == x:
  run_x_code()
elif target == y:
  run_y_code()
else:
  run_z_code()

假设该代码上线时是正确的, target 只会是 x, y, z 三种情况, 但是稍后如果需求改变了, target 允许 w 的 情况出现. 当 target 为 w 时该代码就会错误地调用 run_z_code, 这通常会引起糟糕的后果.

  • 使用 assert 来确保不变量

assert target in (x, y, z)
if target == x:
  run_x_code()
elif target == y:
  run_y_code()
else:
  assert target == z
  run_z_code()

不使用 assert 的场景:

  • 不使用 assert 在校验用户输入的数据, 需要校验的情况下应该抛出异常

  • 不将 assert 用于允许正常失败的情况, 将 assert 用于检查不允许失败的情况.

  • 用户不应该直接看到 AssertionError, 如果用户可以看到, 将这种情况视为一个 BUG

避免使用 magic number

赋予特殊的常量一个名字, 避免重复地直接使用它们的字面值. 合适的时候使用枚举值 Enum.

使用常量在重构时只需要修改一个地方, 如果直接使用字面值在重构时将修改所有使用到的地方.

  • 建议

GRAVITATIONAL_CONSTANT = 9.81

def get_potential_energy(mass, height):
  return mass * height * GRAVITATIONAL_CONSTANT

class ConfigStatus:
  ENABLED = 1
  DISABLED = 0

Config.objects.filter(enabled=ConfigStatus.ENABLED)
  • 不建议

def get_potential_energy(mass, height):
  return mass * height * 9.81

# Django ORM
Config.objects.filter(enabled=1)

处理字典 key 不存在时的默认值

使用 dict.setdefault 或者 defaultdict

# group words by frequency
words = [(1, 'apple'), (2, 'banana'), (1, 'cat')]
frequency = {}

dict.setdefault

  • 建议

for freq, word in words:
  frequency.setdefault(freq, []).append(word)

或者使用 defaultdict

from collections import defaultdict

frequency = defaultdict(list)

for freq, word in words:
  frequency[freq].append(word)
  • 不建议

for freq, word in words:
  if freq not in frequency:
      frequency[freq] = []
  frequency[freq].append(word)

注意在 Python 3 中 map filter 返回的是生成器而不是列表, 在隋性计算方面有所区别

禁止使用 import *

原则上禁止避免使用 import *, 应该显式地列出每一个需要导入的模块

使用 import * 会污染当前命名空间的变量, 无法找到变量的定义是来哪个模块, 在被 import 的模块上的改动可 能会在预期外地影响到其它模块, 可能会引起难以排查的问题.

在某些必须需要使用或者是惯用法 from foo import * 的场景下, 应该在模块 foo 的末尾使用 all 控制被导出的变量.

# foo.py
CONST_VALUE = 1
class Apple:
  ...

__all__ = ("CONST_VALUE", "Apple")

# bar.py
# noinspection PyUnresolvedReferences
from foo import *

作者:未来现相
来源:https://mp.weixin.qq.com/s/QinR-bHolVlr0z8IyhCqfg

收起阅读 »

从零到一编写 IOC 容器

前言本文的编写主要是最近在使用 midway 编写后端应用,midway 的 IOC 控制反转能力跟我们平时常写的前端应用,例如 react、vue 这些单应用还是有蛮大区别的,所以促使我想一探究竟,这种类 Spring IOC 容器是如何用 JavaScri...
继续阅读 »




前言

本文的编写主要是最近在使用 midway 编写后端应用,midway 的 IOC 控制反转能力跟我们平时常写的前端应用,例如 react、vue 这些单应用还是有蛮大区别的,所以促使我想一探究竟,这种类 Spring IOC 容器是如何用 JavaScript 来实现的。为方便读者阅读,本文的组织结构依次为 TS 装饰器、Reflect Metadata、IOC 容器源码简单解读、以及自定义实现 IOC 容器。阅读完本文后,我希望你能有这样的感悟:元数据(metadata)和 装饰器(Decorator) 本是 ES 中两个独立的部分,但是结合它们, 竟然能实现 控制反转 这样的能力。本文的所有演示实例都已经上传到 github 仓库 ioc-container ,读者可以克隆下来进行调试运行。

辛苦整理良久,还望手动点赞鼓励~ 博客 github地址为:github.com/fengshi123/… ,汇总了作者的所有博客,欢迎关注及 star ~

一、TS 装饰器

1、类装饰器

(1)类型声明

type ClassDecorator = <TFunction extends Function>
(target: TFunction) => TFunction | void;
  • 参数:

    target: 类的构造器。

  • 返回:

如果类装饰器返回了一个值,它将会被用来代替原有的类构造器的声明。因此,类装饰器适合用于继承一个现有类并添加一些属性和方法。例如我们可以添加一个 toString 方法给所有的类来覆盖它原有的 toString 方法,以及增加一个新的属性 school,如下所示

type Consturctor = { new (...args: any[]): any };

function School<T extends Consturctor>(BaseClass: T) {
 // 新构造器继承原有的构造器,并且返回
 return class extends BaseClass {  
   // 新增属性 school
   public school = 'qinghua'
   // 重写方法 toString
   toString() {
     return JSON.stringify(this);
  }
};
}

@School
class Student {
 public name = 'tom';
 public age = 14;
}

console.log(new Student().toString())
// {"name":"tom","age":14,"school":"qinghua"}

但是存在一个问题:装饰器并没有类型保护,这意味着在类装饰器的构造函数中新增的属性,通过原有的类实例将报无法找到的错误,如下所示

type Consturctor = { new (...args: any[]): any };

function School<T extends Consturctor>(BaseClass: T){
 return class extends BaseClass {
   // 新增属性 school
   public school = 'qinghua'
};
}


@School
class Student{
 getSchool() {
   return this.school; // Property 'school' does not exist on type 'Student'
}
}

new Student().school  // Property 'school' does not exist on type 'Student'

这是 一个TypeScript的已知的缺陷。 目前我们能做的可以额外提供一个类用于提供类型信息,如下所示

type Consturctor = { new (...args: any[]): any };

function School<T extends Consturctor>(BaseClass: T){
 return class extends BaseClass {
   // 新增属性 school
   public school = 'qinghua'
};
}

// 新增一个类用于提供类型信息
class Base {
 school: string;
}

@School
class Student extends Base{
 getSchool() {
   return this.school;
}
}

new Student().school)

2、属性装饰器

(1)类型声明

type PropertyDecorator = (
target: Object,
 propertyKey: string | symbol
) => void;
复制代码
  • 参数:

    1. target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链。

    2. propertyKey: 属性的名称。

  • 返回:

返回的结果将被忽略。

我们可以通过属性装饰器给属性添加对应的验证判断,如下所示

function NameObserve(target: Object, property: string): void {
 console.log('target:', target)
 console.log('property:', property)
 let _property = Symbol(property)
 Object.defineProperty(target, property, {
   set(val){
     if(val.length > 4){
       throw new Error('名称不能超过4位!')
    }
     this[_property] = val;
  },
   get: function() {
     return this[_property];
}
})
}

class Student {
 @NameObserve
 public name: string;  // target: Student {}   key: 'name'
}

const stu = new Student();
stu.name = 'jack'
console.log(stu.name); // jack
// stu.name = 'jack1'; // Error: 名称不能超过4位!

export default Student;

3、方法装饰器

(1)类型声明:

type MethodDecorator = <T>(
 target: Object,
 propertyKey: string | symbol,
 descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;
  • 参数:

    1. target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链;

    2. propertyKey: 属性的名称;

    3. descriptor: 属性的描述器;

  • 返回: 如果返回了值,它会被用于替代属性的描述器。

方法装饰器不同于属性装饰器的地方在于 descriptor 参数。 通过这个参数我们可以修改方法原本的实现,添加一些共用逻辑。 例如我们可以给一些方法添加打印输入与输出的能力

function logger(target: Object, property: string, 
   descriptor: PropertyDescriptor): PropertyDescriptor | void {
 const origin = descriptor.value;
 console.log(descriptor)
 descriptor.value = function(...args: number[]){
   console.log('params:', ...args)
   const result = origin.call(this, ...args);
   console.log('result:', result);
   return result;
}
}

class Person {
 @logger
 add(x: number, y: number){
   return x + y;
}
}

const person = new Person();
const result = person.add(1, 2);
console.log('查看 result:', result) // 3

4、访问器装饰器

访问器装饰器总体上讲和方法装饰器很接近,唯一的区别在于描述器中有的 key 不同: 方法装饰器的描述器的 key 为:

  • value

  • writable

  • enumerable

  • configurable

访问器装饰器的描述器的key为:

  • get

  • set

  • enumerable

  • configurable

例如,我们可以对访问器进行统一更改:

function descDecorator(target: Object, property: string, 
   descriptor: PropertyDescriptor): PropertyDescriptor | void {
 const originalSet = descriptor.set;
 const originalGet = descriptor.get;
 descriptor.set = function(value: any){
   return originalSet.call(this, value)
}
 descriptor.get = function(): string{
   return 'name:' + originalGet.call(this)
}
}

class Person {
 private _name = 'tom';

 @descDecorator
 set name(value: string){
   this._name = value;
}

 get name(){
   return this._name;
}
}

const person = new Person();
person.name = ('tom');
console.log('查看:', person.name) // name:'tom'

5、参数装饰器

类型声明:

type ParameterDecorator = (
 target: Object,
 propertyKey: string | symbol,
 parameterIndex: number
) => void;
  • 参数:

    1. target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链。

    2. propertyKey: 属性的名称(注意是方法的名称,而不是参数的名称)。

    3. parameterIndex: 参数在方法中所处的位置的下标。

  • 返回:

返回的值将会被忽略。

单独的参数装饰器能做的事情很有限,它一般都被用于记录可被其它装饰器使用的信息。

function ParamDecorator(target: Object, property: string, 
   paramIndex: number): void {
 console.log(property);
 console.log(paramIndex);
}

class Person {
 private name: string;

 public setNmae(@ParamDecorator school: string, name: string){  // setNmae 0
   this.name = school + '_' + name
}
}

6、执行时机

装饰器只在解释执行时应用一次,如下所示,这里的代码会在终端中打印 apply decorator,即便我们其实并没有使用类 A。

function f(C) {
 console.log('apply decorator')
 return C
}

@f
class A {}

// output: apply decorator

7、执行顺序

不同类型的装饰器的执行顺序是明确定义的:

  • 实例成员:参数装饰器 -> 方法/访问器/属性 装饰器

  • 静态成员:参数装饰器 -> 方法/访问器/属性 装饰器

  • 构造器:参数装饰器

  • 类装饰器

示例如下所示

function f(key: string): any {
 console.log("evaluate: ", key);
 return function () {
   console.log("call: ", key);
};
}

@f("Class Decorator")
class C {
 @f("Static Property")
 static prop?: number;

 @f("Static Method")
 static method(@f("Static Method Parameter") foo:any) {}

 constructor(@f("Constructor Parameter") foo:any) {}

 @f("Instance Method")
 method(@f("Instance Method Parameter") foo:any) {}

 @f("Instance Property")
 prop?: number;
}

/* 输出顺序如下
evaluate: Instance Method
evaluate: Instance Method Parameter
call: Instance Method Parameter
call: Instance Method
evaluate: Instance Property
call: Instance Property
evaluate: Static Property
call: Static Property
evaluate: Static Method
evaluate: Static Method Parameter
call: Static Method Parameter
call: Static Method
evaluate: Class Decorator
evaluate: Constructor Parameter
call: Constructor Parameter
call: Class Decorator
*/

我们从上注意到执行实例属性 prop 晚于实例方法 method 然而执行静态属性 static prop 早于静态方法static method。 这是因为对于属性/方法/访问器装饰器而言,执行顺序取决于声明它们的顺序。 然而,同一方法中不同参数的装饰器的执行顺序是相反的, 最后一个参数的装饰器会最先被执行。

function f(key: string): any {
 console.log("evaluate: ", key);
 return function () {
   console.log("call: ", key);
};
}

class C {
 method(
   @f("Parameter Foo") foo,
   @f("Parameter Bar") bar
) {}
}

/* 输出顺序如下
evaluate: Parameter Foo
evaluate: Parameter Bar
call: Parameter Bar
call: Parameter Foo
*/

8、多个装饰器组合

我们可以对同一目标应用多个装饰器。它们的组合顺序为:

  • 求值外层装饰器

  • 求值内层装饰器

  • 调用内层装饰器

  • 调用外层装饰器

如下示例所示

function f(key: string) {
 console.log("evaluate: ", key);
 return function () {
   console.log("call: ", key);
};
}

class C {
 @f("Outer Method")
 @f("Inner Method")
 method() {}
}

/* 输出顺序如下
evaluate: Outer Method
evaluate: Inner Method
call: Inner Method
call: Outer Method
*/

二、Reflect Metadata

1、背景

在 ES6 的规范当中,ES6 支持元编程,核心是因为提供了对 Proxy 和 Reflect 对象的支持。简单来说这个 API 的作用就是可以实现对变量操作的函数化,也就是反射。然而 ES6 的 Reflect 规范里面还缺失一个规范,那就是 Reflect Metadata。这会造成什么样的情境呢? 由于 JS/TS 现有的 装饰器更多的是存在于对函数或者属性进行一些操作,比如修改他们的值,代理变量,自动绑定 this 等等功能。但是却无法实现通过反射来获取究竟有哪些装饰器添加到这个类/方法上... 这就限制了 JS 中元编程的能力。 此时 Relfect Metadata 就派上用场了,可以通过装饰器来给类添加一些自定义的信息。然后通过反射将这些信息提取出来(当然你也可以通过反射来添加这些信息)。 综合一下, JS 中对 Reflect Metadata 的诉求,简单概括就是:

  • 其他 C#、Java、Pythone 语言已经有的高级功能,我 JS 也应该要有(诸如C# 和 Java 之类的语言支持将元数据添加到类型的属性或注释,以及用于读取元数据的反射API,而目前 JS 缺少这种能力)

  • 许多用例(组合/依赖注入,运行时类型断言,反射/镜像,测试)都希望能够以一致的方式向类中添加其他元数据。

  • 为了使各种工具和库能够推理出元数据,需要一种标准一致的方法;

  • 元数据不仅可以用在对象上,也可以通过相关捕获器用在 Proxy 上;

  • 对开发人员来说,定义新的元数据生成装饰器应该简洁易用;

2、使用

TypeScript 在 1.5+ 的版本已经支持 reflect-metadata,但是我们在使用的时候还需要额外进行安装,如下所示

  • npm i reflect-metadata --save

  • 在 tsconfig.json 里配置选项 emitDecoratorMetadata: true

关于 reflect-metadata 的基本使用 api 可以阅读 reflect-metadata 文档,其包含常见的增删改查基本功能,我们来看下其基本的使用示例,其中 Reflect.metadata 当作 Decorator 使用,当修饰类时,在类上添加元数据,当修饰类属性时,在类原型的属性上添加元数据,如:

import "reflect-metadata";

@Reflect.metadata('classMetaData', 'A')
class SomeClass {
 @Reflect.metadata('methodMetaData', 'B')
 public someMethod(): string {
   return 'hello someMethod';
}
}

console.log(Reflect.getMetadata('classMetaData', SomeClass)); // 'A'
console.log(Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod')); // 'B

当然跟我们平时看到的 IOC 不同,我们进一步结合装饰器,如下所示,与前面的功能是一样的

import "reflect-metadata";

function classDecorator(): ClassDecorator {
 return target => {
   // 在类上定义元数据,key 为 `classMetaData`,value 为 `a`
   Reflect.defineMetadata('classMetaData', 'A', target);
};
}

function methodDecorator(): MethodDecorator {
 return (target, key, descriptor) => {
   // 在类的原型属性 'someMethod' 上定义元数据,key 为 `methodMetaData`,value 为 `b`
   Reflect.defineMetadata('methodMetaData', 'B', target, key);
};
}

@classDecorator()
class SomeClass {
 @methodDecorator()
 someMethod() {}
}

console.log(Reflect.getMetadata('classMetaData', SomeClass)); // 'A'
console.log(Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod')); // 'B'

3、design:类型元数据

在 TS 中的 reflect-metadata 的功能是经过增强的,其添加 "design:type"、"design:paramtypes" 和 "design:returntype" 这 3 个类型相关的元数据

  • design:type 表示被装饰的对象是什么类型, 比如是字符串、数字、还是函数等;

  • design:paramtypes 表示被装饰对象的参数类型, 是一个表示类型的数组, 如果不是函数, 则没有该 key;

  • design:returntype 表示被装饰对象的返回值属性, 比如字符串、数字或函数等;

示例如下所示

import "reflect-metadata";

@Reflect.metadata('type', 'class')
class A {  
 constructor(
   public name: string,
   public age: number
) { }  

 @Reflect.metadata(undefined, undefined)  
 method(name: string, age: number):boolean {    
   return true  
}
}

 const t1 = Reflect.getMetadata('design:type', A.prototype, 'method')
 const t2 = Reflect.getMetadata('design:paramtypes', A.prototype, 'method')
 const t3 = Reflect.getMetadata('design:returntype', A.prototype, 'method')
 
 console.log(t1)  // [Function: Function]
 console.log(...t2) // [Function: String] [Function: Number]
 console.log(t3) // [Function: Boolean]

三、IOC 容器实现

1、源码解读

我们可以从 github 上克隆 midway 仓库的代码到本地,然后进行代码阅读以及 debug 调试。本篇博文我们主要想探究下 midway 的依赖注入合控制反转是如何实现的,其主要源码存在于两个目录:packages/core 和 packages/decorator,其中 packages/core 包含依赖注入的核心实现,加载对象的class,同步、异步创建对象实例化,对象的属性绑定等。 IOC 容器就像是一个对象池,管理着每个对象实例的信息(Class Definition),所以用户无需关心什么时候创建,当用户希望拿到对象的实例 (Object Instance) 时,可以直接拿到依赖对象的实例,容器会 自动将所有依赖的对象都自动实例化。packages/core 中主要有以下几种,分别处理不同的逻辑:

  • AppliationContext 基础容器,提供了基础的增加定义和根据定义获取对象实例的能力;

  • MidwayContainer 用的最多的容器,做了上层封装,通过 bind 函数能够方便的生成类定义,midway 从此类开始扩展;

  • RequestContainer 用于请求链路上的容器,会自动销毁对象并依赖另一个容器创建实例;

packages/decorator 包含装饰器 provide.ts、inject.ts 的实现,在midwayjs中是有一个装饰器管理类DecoratorManager, 用来管理 midwayjs 的所有装饰器:

  • @provide() 的作用是简化绑定,能被 IOC 容器自动扫描,并绑定定义到容器上,对应的逻辑是绑定对象定义;

  • @inject() 的作用是将容器中的定义实例化成一个对象,并且绑定到属性中,这样,在调用的时候就可以访问到该属性。

2、简单实现

2.1、装饰器 Provider

实现装饰器 Provider 类,作用为将对应类注册到 IOC 容器中。

import 'reflect-metadata'
import * as camelcase from 'camelcase'
import { class_key } from './constant'

// Provider 装饰的类,表示要注册到 IOC 容器中
export function Provider (identifier?: string, args?: Array<any>) {
 return function (target: any) {
   // 类注册的唯一标识符
   identifier = identifier ?? camelcase(target.name)

   Reflect.defineMetadata(class_key, {
     id: identifier,  // 唯一标识符
     args: args || [] // 实例化所需参数
  }, target)
   return target
}
}

2.2、装饰器 Inject

实现装饰器 Inject 类,作用为将对应的类注入到对应的地方。

import 'reflect-metadata'
import { props_key } from './constant'

export function Inject () {
 return function (target: any, targetKey: string) {
   // 注入对象
   const annotationTarget = target.constructor
   let props = {}
   // 同一个类,多个属性注入类
   if (Reflect.hasOwnMetadata(props_key, annotationTarget)) {
     props = Reflect.getMetadata(props_key, annotationTarget)
  }

   //@ts-ignore
   props[targetKey] = {
     value: targetKey
  }

   Reflect.defineMetadata(props_key, props, annotationTarget)
}
}

2.3、管理容器 Container

管理容器 Container 的实现,用于绑定实例信息并且在对应的地方获取它们。

import 'reflect-metadata'
import { props_key } from './constant'

export class Container {
 bindMap = new Map()

 // 绑定类信息
 bind(identifier: string, registerClass: any, constructorArgs: any[]) {
   this.bindMap.set(identifier, {registerClass, constructorArgs})
}

 // 获取实例,将实例绑定到需要注入的对象上
 get<T>(identifier: string): T {
   const target = this.bindMap.get(identifier)
   if (target) {
     const { registerClass, constructorArgs } = target
     // 等价于 const instance = new registerClass([...constructorArgs])
     const instance = Reflect.construct(registerClass, constructorArgs)

     const props = Reflect.getMetadata(props_key, registerClass)
     for (let prop in props) {
       const identifier = props[prop].value
       // 递归进行实例化获取 injected object
       instance[prop] = this.get(identifier)
    }
     return instance
  }
}
}

2.4、加载类文件 load

启动时扫描所有文件,获取文件导出的所有类,然后根据元数据进行绑定。

import * as fs from 'fs'
import { resolve } from 'path'
import { class_key } from './constant'

// 启动时扫描所有文件,获取定义的类,根据元数据进行绑定
export function load(container: any, path: string) {
 const list = fs.readdirSync(path)
 for (const file of list) {
   if (/\.ts$/.test(file)) {
     const exports = require(resolve(path, file))

     for (const m in exports) {
       const module = exports[m]
       if (typeof module === 'function') {
         const metadata = Reflect.getMetadata(class_key, module)
         // register
         if (metadata) {
           container.bind(metadata.id, module, metadata.args)
        }
      }
    }
  }
}
}

2.5、示例类

三个示例类如下所示

// class A
import { Provider } from "../provide";
import { Inject } from "../inject";
import B from './classB'
import C from './classC'

@Provider('a')
export default class A {
 @Inject()
 private b: B

 @Inject()
 c: C

 print () {
   this.c.print()
}
}

// class B
import { Provider } from '../provide'

@Provider('b', [10])
export default class B {
 n: number
 constructor (n: number) {
   this.n = n
}
}

// class C
import { Provider } from '../provide'

@Provider()
export default class C {
 print () {
   console.log('hello')
}
}

2.6、初始化

我们能从以下示例结果中看到,我们已经实现了一个基本的 IOC 容器能力。

import { Container } from './container'
import { load } from './load'
import { class_path } from './constant'

const init =  function () {

 const container = new Container()
 // 通过加载,会先执行装饰器(设置元数据),
 // 再由 container 统一管理元数据中,供后续使用
 load(container, class_path)
 const a:any = container.get('a') // A { b: B { n: 10 }, c: C {} }
 console.log(a);
 a.c.print() // hello
}

init()

总结

本文的依次从 TS 装饰器、Reflect Metadata、IOC 容器源码简单解读、以及自定义实现 IOC 容器四个部分由零到一编写自定义 IOC 容器,希望对你有所启发。本文的所有演示实例都已经上传到 github 仓库 ioc-container ,读者可以克隆下来进行调试运行。

作者:我是你的超级英雄
来源:https://juejin.cn/post/7036895697865555982

收起阅读 »

300行代码实现循环滚动控件

序言在业务中需要显示一个循环滚动的控件,内容可以循环滚动,可以自动滚动,手指触摸的时候会暂停。 由于目前的方案都是基于ViewPager或者RecycleView的。还需要实现Adapter,需要拦截各种事件。使用成本比较高。于是我就自定义了一个控件实现该功能...
继续阅读 »

序言

在业务中需要显示一个循环滚动的控件,内容可以循环滚动,可以自动滚动,手指触摸的时候会暂停。 由于目前的方案都是基于ViewPager或者RecycleView的。还需要实现Adapter,需要拦截各种事件。使用成本比较高。于是我就自定义了一个控件实现该功能,

使用

使用起来很简单。把需要显示的控件放置在其中就行。就和普通的HorizontalScrollView用法一样。 不过子控件必须要LoopLinearLayout 在这里插入图片描述

效果

  • 1.支持左右循环滚动
  • 2.支持自动滚动
  • 3.支持点击事件
  • 4.触摸暂停
  • 5.支持惯性滚动
  • 6.一共不到300行代码,逻辑简单易于扩展

在这里插入图片描述

原理

通过继承自HorizontalScrollView实现,重新onOverScrolled 和 scrollTo 方法在调用supper方法之前,对是否到达边界进行判断,如果到达就调用LoopLinearLayout.changeItemsToRight() 方法对内容重新摆放。

摆放使用的是 child.layout() 的方法,没有性能问题。摆放完成以后,对scrollX进行重新赋值。

需要注意的是在HorizontalScrollView中有一个负责惯性滚动的OverScroller 在这里插入图片描述 但是在调用其fling方法之前会设置maxX这导致无法滚动到控件内容之外。所以使用反射修改了这个类。拦截了fling方法 在这里插入图片描述

而动画的时长设置的是滚动一个LoopScrollView宽度需要的时间。还有就是无限循环的动画需要在 onDetachedFromWindow中移除,避免内存泄漏

源码

LoopLinearLayout

package com.example.myapplication;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;

import androidx.annotation.Nullable;

import java.util.List;

/**
* Created by zhuguohui
* Date: 2021/11/30
* Time: 10:46
* Desc:
*/
public class LoopLinearLayout extends LinearLayout {
public LoopLinearLayout(Context context) {
this(context, null);
}

public LoopLinearLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}


public void changeItemsToRight(List<View> moveItems, int offset) {

int offset2 = 0;
for (int i = 0; i < getChildCount(); i++) {
View childAt = getChildAt(i);
if (!moveItems.contains(childAt)) {
MarginLayoutParams layoutParams = (MarginLayoutParams) childAt.getLayoutParams();
offset2 += childAt.getWidth() + layoutParams.leftMargin + layoutParams.rightMargin;
childAt.layout(childAt.getLeft() - offset, childAt.getTop(), childAt.getRight() - offset, childAt.getBottom());
}
}
for(View view:moveItems){
view.layout(view.getLeft()+offset2,view.getTop(),view.getRight()+offset2,view.getBottom());
}
}
public void changeItemsToLeft(List<View> moveItems, int offset) {

int offset2 = 0;
for (int i = 0; i < getChildCount(); i++) {
View childAt = getChildAt(i);
if (!moveItems.contains(childAt)) {
MarginLayoutParams layoutParams = (MarginLayoutParams) childAt.getLayoutParams();
offset2 += childAt.getWidth() + layoutParams.leftMargin + layoutParams.rightMargin;
childAt.layout(childAt.getLeft() + offset, childAt.getTop(), childAt.getRight() + offset, childAt.getBottom());
}
}
for(View view:moveItems){
view.layout(view.getLeft()-offset2,view.getTop(),view.getRight()-offset2,view.getBottom());
}
}


}

LoopScrollView

package com.example.myapplication;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.LinearInterpolator;
import android.widget.HorizontalScrollView;
import android.widget.OverScroller;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

public class LoopScrollView extends HorizontalScrollView {

private LoopScroller loopScroller;
private ValueAnimator animator;

public LoopScrollView(Context context) {
this(context, null);
}

public LoopScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
setOverScrollMode(OVER_SCROLL_ALWAYS);
try {
@SuppressLint("DiscouragedPrivateApi")
Field field =HorizontalScrollView.class.getDeclaredField("mScroller");
field.setAccessible(true);
loopScroller = new LoopScroller(getContext());
field.set(this, loopScroller);

} catch (Exception e) {
e.printStackTrace();
}

}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if(changed||animator==null){
buildAnimation();
}
}

private void buildAnimation() {
if(animator!=null){
animator.cancel();
animator=null;
}
animator = ValueAnimator.ofInt(getWidth() - getPaddingRight() - getPaddingLeft());
animator.setDuration(5*1000);
animator.setRepeatCount(-1);
animator.setInterpolator(new LinearInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
int lastValue;
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value= (int) animation.getAnimatedValue();
int scrollByX=value-lastValue;
// Log.i("zzz","scroll by x="+scrollByX);
scrollByX=Math.max(0,scrollByX);
if(userUp) {
scrollBy(scrollByX, 0);
}
lastValue=value;
}
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
}

@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
}

});
animator.start();
}

static class LoopScroller extends OverScroller{
public LoopScroller(Context context) {
super(context);
}

@Override
public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, int overX, int overY) {
super.fling(startX, startY, velocityX, velocityY, Integer.MIN_VALUE,Integer.MAX_VALUE, minY, maxY, 0, overY);
}
}




@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if(animator!=null){
animator.cancel();
animator.removeAllListeners();
animator = null;
}
}

@Override
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
if (userUp) {
//scroller再滚动
scrollX=loopScroller.getCurrX();
int detailX = scrollX - lastScrollX;
lastScrollX = scrollX;
if(detailX==0){
return;
}
scrollX = detailX + getScrollX();

}
int moveTo = moveItem(scrollX,clampedX);

super.onOverScrolled(moveTo, scrollY, false, clampedY);
}

boolean userUp = true;
int lastScrollX = 0;

@Override
public boolean onTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_UP) {
userUp = true;
lastScrollX = getScrollX();
} else {
userUp = false;
}
return super.onTouchEvent(ev);
}
@Override
public void scrollTo(int x, int y) {
int scrollTo = moveItem(x, false);
super.scrollTo(scrollTo, y);
}


private int moveItem(int scrollX, boolean clampedX) {

int toScrollX = scrollX;

if (getChildCount() > 0) {
if (!canScroll(scrollX,clampedX)) {
boolean toLeft=scrollX<=0;
int mWidth=getWidth()-getPaddingLeft()-getPaddingRight();
//无法向右滚动了,将屏幕外的item,移动到后面
List<View> needRemoveViewList = new ArrayList<>();
LoopLinearLayout group = (LoopLinearLayout) getChildAt(0);
int removeItemsWidth = 0;
boolean needRemove = false;
for (int i = group.getChildCount() - 1; i >= 0; i--) {
View itemView = group.getChildAt(i);
MarginLayoutParams params = (MarginLayoutParams) itemView.getLayoutParams();
if(toLeft){
int itemLeft = itemView.getLeft() - params.leftMargin;
if (itemLeft >= mWidth) {
//表示之后的控件都需要移除
needRemove = true;
}
}else{
int itemRight = itemView.getRight() + params.rightMargin;
if (itemRight <= scrollX) {
//表示之后的控件都需要移除
needRemove = true;
}
}

if (needRemove) {
int itemWidth = itemView.getWidth() + params.rightMargin + params.leftMargin;
removeItemsWidth += itemWidth;
needRemoveViewList.add(0,itemView);
}
needRemove=false;
}
if(!toLeft){
group.changeItemsToRight(needRemoveViewList,removeItemsWidth);
toScrollX -=removeItemsWidth;
}else{
group.changeItemsToLeft(needRemoveViewList,removeItemsWidth);
toScrollX +=removeItemsWidth;
}

}

}
return Math.max(0, toScrollX);
}

private boolean canScroll(int scrollX, boolean clampedX) {
if(scrollX<0){
return false;
}
if(scrollX==0&&clampedX){
//表示向左划不动了
return false;
}
View child = getChildAt(0);
if (child != null) {
int childWidth = child.getWidth();
return getWidth() + scrollX < childWidth + getPaddingLeft() + getPaddingRight();
}
return false;
}
}

最后所有的功能只依赖上述两个类,关于动画的时长写死在类中的,没有抽成方法。有需要的自己去改吧。


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

收起阅读 »

synchronized 的实现原理

synchronized 的使用 锁代码块(锁对象可指定,可为this、XXX.class、全局变量) 锁普通方法(锁对象是this,即该类实例本身) 锁静态方法(锁对象是该类,即XXX.class) 锁代码块 public class Sync { ...
继续阅读 »

synchronized 的使用



  • 锁代码块(锁对象可指定,可为this、XXX.class、全局变量)

  • 锁普通方法(锁对象是this,即该类实例本身)

  • 锁静态方法(锁对象是该类,即XXX.class)


锁代码块


public class Sync {

private int a = 0;

public void add(){
synchronized (this){
System.out.println("a values "+ ++a);
}
}

}

反编译之后的


public add()V
TRYCATCHBLOCK L0 L1 L2 null
TRYCATCHBLOCK L2 L3 L2 null
ALOAD 0
DUP
ASTORE 1
MONITORENTER
L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "a values "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 0
DUP
GETFIELD com/arrom/webview/Sync.a : I
ICONST_1
IADD
DUP_X1
PUTFIELD com/arrom/webview/Sync.a : I
INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
ALOAD 1
MONITOREXIT
L1
GOTO L4
L2
ASTORE 2
ALOAD 1
MONITOREXIT
L3
ALOAD 2
ATHROW
L4
RETURN
MAXSTACK = 5
MAXLOCALS = 3
}

由反编译结果可以看出:synchronized代码块主要是靠MONITORENTERMONITOREXIT这两个原语来实现同步的。当线程进入MONITORENTER获得执行代码的权利时,其他线程就不能执行里面的代码,直到锁Owner线程执行MONITOREXIT释放锁后,其他线程才可以竞争获取锁。


MONITORENTER

每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权.



  1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

  2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.

  3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。


第2点就涉及到了可重入锁,意思就是说当一个线程已经获取一个锁时,它可以再获取无数次,从代码的角度上将就是有无数个相同的synchronized语句块嵌套在一起。在进入时,monitor的进入数+1;退出时就-1,直到为0的时候才可以被其他线程竞争获取。


MONITOREXIT

执行MONITOREXIT的线程必须是objectref所对应的monitor的所有者。


指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。


锁普通方法


public class Sync {

private int a = 0;

public synchronized void add(){
System.out.println("a values "+ ++a);
}

}

反编译之后并没有monitorenter和monitorexit,但是常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:


当方法调用时会检查方法的ACC_SYNCHRONIZED之后才能执行方法体,方法执行完后再释放monitor。


在方法执行期间,其他任何线程都无法再获得同一个monitor对象。这种方式与语句块没什么本质区别,都是通过竞争monitor的方式实现的。只不过这种方式是隐式的实现方法。


MONITORENTER和ACC_SYNCHRONIZED只是起标志作用,并无实质操作。


锁静态方法



private static int a = 0;

public synchronized static void add(){
System.out.println("a values "+ ++a);
}

常量池中用ACC_STATIC标志了这是一个静态方法,然后用ACC_SYNCHRONIZED标志位提醒线程去竞争monitor。由于静态方法是属于类级别的方法(即不用创建对象就可以被调用),所以这是一个类级别(XXX.class)的锁,即竞争某个类的monitor。


锁的竞争过程


image.png



  • (1)、多个线程请求锁,首先进入Contention List,它可以接纳所有请求线程,而且是一个后进先出(LIFO)的虚拟队列,通过结点Node和next指针构造。

  • (2)(3)、ContentionList会被线程并发访问,EntryList为了降低线程对ContentionList队尾的争用而构造出来。当Owner释放锁时,会从ContentionList中迁移线程到EntryList,并会指定EntryList中的某个线程(一般为Head结点)为Ready Thread,也就是说某个时刻最多只有一个线程正在竞争锁。

  • (4)、Owner并不是直接把锁交给OnDeck线程,而是将竞争锁的权利交给OnDeck(将锁释放了),然后让OnDeck自己去竞争。竞争成功后,OnDeck线程就变成Owner;否则继续留在EntryList的队头。

  • (5)(6)、当线程调用wait方法被阻塞时,进入WaitSet;当其他线程调用notifyAll()(notify())方法后,阻塞队列的(某个)线程就会进入EntryList中。


      处于ContetionList、EntryList、WaitSet的线程均处于阻塞状态。而线程被阻塞涉及到用户态与内核态的切换(Liunx),系统切换严重影响锁的性能。解决这个问题的办法就是自旋。自旋就是线程不断进行内部循环,即for循环什么也不做,防止线程wait()阻塞,在自旋过程中不断尝试获取锁,如果自旋期间,Owner刚好释放锁,此时自旋线程就可以去竞争锁。如果自旋了一段时间还没获取到锁,那没办法,只能调用wait()阻塞了。 

为什么自旋了一段时间后又调用wait()方法呢?因为自旋是要消耗CPU的,而且还有线程上下文切换,因为CPU还可以调度线程,只不过执行的是空的for循环罢了。 

对自旋锁周期的选择上,HotSpot认为最佳时间应是一个线程上下文切换的时间,但目前并没有做到。 

所以,synchronized是什么时候进行自旋的?答案是在进入ContetionList之前,因为它自旋一定时间后还没获取锁,最后它只好在ContetionList中阻塞等待了。


对象头


对象头(Object Header)包括两部分信息。


一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,这部分数据的长度在32位和64位的虚拟机(暂 不考虑开启压缩指针的场景)中分别为32个和64个Bits,官方称它为“Mark Word”。


对象需要存储的运行时数据很多,其实已经超出了32、64位Bitmap结构所能记录的限度,但是对象头信息是与对象自身定义的数据无关的额 外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。例如在32位的HotSpot虚拟机 中对象未被锁定的状态下,Mark Word的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志 位,1Bit固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如下表所示。


image.png


另外一部分是类型指针,即是对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说查找对象的元数据信息并不一定要经过对象本身。另外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。


为了减少锁释放带来的消耗,锁有一个升级的机制,从轻到重依次是:无锁状态 ——> 偏向锁 ——> 轻量级锁 ——>重量级锁。


偏向锁


在无其它线程与它竞争的情况下,持有偏向锁的线程永远也不需要同步。


它的加锁过程很简单:线程访问同步代码块时检查偏向锁中线程ID是否指向自己,如果是表明该线程已获得锁;否则,检测偏向锁标记是否为1,不是的话则CAS竞争锁,如果是就将对象头中线程ID指向自己。


当存在线程竞争锁时,偏向锁才会撤销,转而升级为轻量级锁。而这个撤销过程则需要有一个全局安全点(即这个时间点上没有正在执行的字节码)


image.png


在撤销锁的时候,栈中对象头的Mark Word要么偏向于其他线程,要么恢复到无锁或者轻量级锁。



  • 优点:加锁和解锁无需额外消耗

  • 缺点:锁进化时会带来额外锁撤销的消耗

  • 适用场景:只有一个线程访问同步代码块


轻量级锁


image.png



  • 优点:竞争的线程不阻塞,也就是不涉及到用户态与内核态的切换(Liunx),减少系统切换锁带来的开销

  • 缺点:如果长时间竞争不到锁,自旋会消耗CPU

  • 适用场景:追求响应时间、同步块执行速度非常快


重量级锁


它是传统意义上的锁,通过互斥量来实现同步,线程阻塞,等待Owner释放锁唤醒。




  • 优点:线程竞争不自旋,不消耗CPU




  • 缺点:线程阻塞,响应时间慢




  • 适用场景:追求吞吐量、同步块执行时间较长


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

Jetpack-Lifecycle

1.AndroidX 的ComponentActivity 实现了LifecycleOwner接口,ComponentActivity 的子类会重写LifecycleOwner的接口方法,以便得到我们使用的lifecycle对象,lifecycle 是在Com...
继续阅读 »

图片.png


1.AndroidX 的ComponentActivity 实现了LifecycleOwner接口,ComponentActivity 的子类会重写LifecycleOwner的接口方法,以便得到我们使用的lifecycle对象,lifecycle 是在ComponentActivity 中创建的lifecycleRegistry对象;


2.使用的时候,通过lifecycleRegistry对象addObserver的方式注册LifecycleObserver,当生命周期变化的时候,会回调LifecycleObserver的onStateChanged, 里面的Lifecycle.Event能监听到当前Activity不同生命周期的变化


原理:


1.lifecycle注册的时候,会初始化observer的状态State,初始状态是 initial或者destroy, 将observer和state封装成一个类作为map的value值, observer作为key;


2.addObserver还有一个状态对齐,等会讲


3.当宿主activity或者fragment生命周期发生变化的时候,会分发当前的生命周期事件,转换成对应的mState,


3.1 和当前map中最早添加的observer的state进行比较,如果mState小于 state的值, 说明当前执行的是OnResume->onPause->onStop->onDestroy中的某一个环节, 遍历当前的map,将map的state向下更新为当前的生命周期状态,并调用observer的onStateChange方法;


3.2 和当前map中最后添加的observer的state进行比较,如果mState大于 state的值, 说明当前执行的是onCreate->onStart->onResume中的某一个环节, 遍历当前的map,将map的state向上更新为当前的生命周期状态,并调用observer的onStateChange方法;


3.3 状态对齐, 比如:当我们在生命周期的onStop方法中去addObserver时,此时添加到map中的state是inital状态, 实际上当前的生命周期是onStop,对应的是Created状态, 此时需要将map中小于Created的状态更新成Created状态,因为是upEvent, 所以回调的event会有onCreate


小结: 1.创建一个state保存到map; 2.等生命周期变化时,更新state的值,回调onStateChanged方法,达到监控生命周期的作用


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

Glide数据输入输出

基础概念 在正式开始之前先明确一些概念 Glide输入: 我们日常在使用Glide的时候,通过load可以加载不同的资源类型例如文件,字符串等待。 我们把load的不同类型称为不同的输入。 Glide输出: Glide RequestManager提供了许多...
继续阅读 »

基础概念


在正式开始之前先明确一些概念


Glide输入: 我们日常在使用Glide的时候,通过load可以加载不同的资源类型例如文件,字符串等待。


requestManager多个load重载.png


我们把load的不同类型称为不同的输入。


Glide输出: Glide RequestManager提供了许多的as重载方法,


GlideAs方法.png


通过不同的as我们可以指定不同的输出类型。


ModelLoader: 是一个泛型接口,最直观的翻译是模型加载器。ModelLoader标记了它能够加载什么类型的数据,以及加载后返回什么样的数据类型。注意这里说说的返回的数据类型并不是我们想要的输出。ModelLoader定义如下


public interface ModelLoader<Model, Data> {
 class LoadData<Data> {
     //数据加载的key
   public final Key sourceKey;
   public final List<Key> alternateKeys;
     //获取数据的接口,对应获取不同类型的数据实现
   public final DataFetcher<Data> fetcher;

   public LoadData(@NonNull Key sourceKey, @NonNull DataFetcher<Data> fetcher) {
     this(sourceKey, Collections.<Key>emptyList(), fetcher);
  }

   public LoadData(@NonNull Key sourceKey, @NonNull List<Key> alternateKeys,
       @NonNull DataFetcher<Data> fetcher) {
     this.sourceKey = Preconditions.checkNotNull(sourceKey);
     this.alternateKeys = Preconditions.checkNotNull(alternateKeys);
     this.fetcher = Preconditions.checkNotNull(fetcher);
  }
}

   //创建LoadData 对象
 @Nullable
 LoadData<Data> buildLoadData(@NonNull Model model, int width, int height,
     @NonNull Options options);
   //判断当前的ModelLoader是否能够处理这个model
 boolean handles(@NonNull Model model);
}

DataFetcher: 用于进行数据加载,不同的类型有不同的DataFetcher


SourceGenerator远程数据加载过程


@Override
public boolean startNext() {
 //...
 boolean started = false;
 while (!started && hasNextModelLoader()) {
   loadData = helper.getLoadData().get(loadDataListIndex++);
   if (loadData != null
       && (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource())
       || helper.hasLoadPath(loadData.fetcher.getDataClass()))) {
     started = true;
     loadData.fetcher.loadData(helper.getPriority(), this);
  }
}
 return started;
}

代码流程:


通过DecodeHelper获取LoadData,遍历每一个LoadData;


查看当前LoadData加载出来的数据能不能,转换成我们想要的输出数据,如果可以的话就是用当前loadData进行数据加载。


DecodeHelpe#getLoadData()


List<LoadData<?>> getLoadData() {
 if (!isLoadDataSet) {
   isLoadDataSet = true;
   loadData.clear();
     //此处的model就是我们 通过调用load传递进来的参数 即输入
   List<ModelLoader<Object, ?>> modelLoaders = glideContext.getRegistry().getModelLoaders(model);
   //noinspection ForLoopReplaceableByForEach to improve perf
   for (int i = 0, size = modelLoaders.size(); i < size; i++) {
     ModelLoader<Object, ?> modelLoader = modelLoaders.get(i);
     //通过modelLoader 构建loadData
     LoadData<?> current =
         modelLoader.buildLoadData(model, width, height, options);
     if (current != null) {
       loadData.add(current);
    }
  }
}
 return loadData;
}

ModelLoaderRegistry#getModelLoaders


getModelLoaders()实现的位置在ModelLoaderRegistry#getModelLoaders


public <A> List<ModelLoader<A, ?>> getModelLoaders(@NonNull A model) {
   //获取对应的modelLoader
 List<ModelLoader<A, ?>> modelLoaders = getModelLoadersForClass(getClass(model));
 int size = modelLoaders.size();
 boolean isEmpty = true;
 List<ModelLoader<A, ?>> filteredLoaders = Collections.emptyList();
 //noinspection ForLoopReplaceableByForEach to improve perf
 for (int i = 0; i < size; i++) {
   ModelLoader<A, ?> loader = modelLoaders.get(i);
     //判断对应的modelLoader是否有能力处理对应的model
   if (loader.handles(model)) {
     if (isEmpty) {
       filteredLoaders = new ArrayList<>(size - i);
       isEmpty = false;
    }
     filteredLoaders.add(loader);
  }
}
 return filteredLoaders;
}

getModelLoadersForClass主要是通过MultiModelLoaderFactory#build。然后MultiModelLoaderFactory会遍历所有已经注册的ModelLoader,只要当前的model是已经注册model的子类或者对应的实现,那么就会把对应的ModelLoader添加到待返回的集合中。


DecodeHelper#hasLoadPath


boolean hasLoadPath(Class<?> dataClass) {
   return getLoadPath(dataClass) != null;
}

<Data> LoadPath<Data, ?, Transcode> getLoadPath(Class<Data> dataClass) {
 return glideContext.getRegistry().getLoadPath(dataClass, resourceClass, transcodeClass);
}

可以看到hasLoadPath代码其实非常简单,就是获取一个LoadPath集合。获取的时候传递了三个参数 DataFetcher加载出来的数据类型dataClass,resourceClass ,transcodeClass


getLoadPath参数


对于resourceClass ,transcodeClass在DecodeHelper定义如下:


private Class<?> resourceClass;
private Class<Transcode> transcodeClass;

他们在init方法中进行初始化,经过层层代码的流转我们发现最终的参数初始化来自于RequestBuilder#obtainRequest


private Request obtainRequest(
     Target<TranscodeType> target,
     RequestListener<TranscodeType> targetListener,
     BaseRequestOptions<?> requestOptions,
     RequestCoordinator requestCoordinator,
     TransitionOptions<?, ? super TranscodeType> transitionOptions,
     Priority priority,
     int overrideWidth,
     int overrideHeight,
     Executor callbackExecutor) {
   return SingleRequest.obtain(
       context,
       glideContext,
       model,
       //该参数会在调用as系列方法后初始化,指向的是我们想要的输出类型。
       transcodeClass,
       //指向的是RequestBuilder 自身
       requestOptions,
       overrideWidth,
       overrideHeight,
       priority,
       target,
       targetListener,
       requestListeners,
       requestCoordinator,
       glideContext.getEngine(),
       transitionOptions.getTransitionFactory(),
       callbackExecutor);
}

而RequestOptions#getResourceClass返回的resourceClass默认情况下返回的是Object,而在asBitmap和asGifDrawable会做其它的转换。


private static final RequestOptions DECODE_TYPE_BITMAP = decodeTypeOf(Bitmap.class).lock();
private static final RequestOptions DECODE_TYPE_GIF = decodeTypeOf(GifDrawable.class).lock();

@NonNull
 @CheckResult
 public RequestBuilder<Bitmap> asBitmap() {
   return as(Bitmap.class).apply(DECODE_TYPE_BITMAP);
}
 public RequestBuilder<GifDrawable> asGif() {
   return as(GifDrawable.class).apply(DECODE_TYPE_GIF);
}

getLoadPath执行过程


getLoadPath最终会调用Registry#getLoadPath


@Nullable
public <Data, TResource, Transcode> LoadPath<Data, TResource, Transcode> getLoadPath(
   @NonNull Class<Data> dataClass, @NonNull Class<TResource> resourceClass,
   @NonNull Class<Transcode> transcodeClass) {
   //先获取DecodePath  
 List<DecodePath<Data, TResource, Transcode>> decodePaths =
       getDecodePaths(dataClass, resourceClass, transcodeClass);
   if (decodePaths.isEmpty()) {
     result = null;
  } else {
     result =
         new LoadPath<>(
             dataClass, resourceClass, transcodeClass, decodePaths, throwableListPool);
  }
   loadPathCache.put(dataClass, resourceClass, transcodeClass, result);
 return result;
}

private <Data, TResource, Transcode> List<DecodePath<Data, TResource, Transcode>> getDecodePaths(
     @NonNull Class<Data> dataClass, @NonNull Class<TResource> resourceClass,
     @NonNull Class<Transcode> transcodeClass) {
   List<DecodePath<Data, TResource, Transcode>> decodePaths = new ArrayList<>();
   //遍历所有资源解码器,获取能够解析当前输入dataClass的解码器
   List<Class<TResource>> registeredResourceClasses =
       decoderRegistry.getResourceClasses(dataClass, resourceClass);
   for (Class<TResource> registeredResourceClass : registeredResourceClasses) {
       //获取能够解析当前输入dataClass且将数据转变成我们想要的transcodeClass类型的转换类
     List<Class<Transcode>> registeredTranscodeClasses =
         transcoderRegistry.getTranscodeClasses(registeredResourceClass, transcodeClass);

     for (Class<Transcode> registeredTranscodeClass : registeredTranscodeClasses) {
//获取对应的所有解码器
       List<ResourceDecoder<Data, TResource>> decoders =
           decoderRegistry.getDecoders(dataClass, registeredResourceClass);
       //转换类
       ResourceTranscoder<TResource, Transcode> transcoder =
           transcoderRegistry.get(registeredResourceClass, registeredTranscodeClass);
       @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
       DecodePath<Data, TResource, Transcode> path =
           new DecodePath<>(dataClass, registeredResourceClass, registeredTranscodeClass,
               decoders, transcoder, throwableListPool);
       decodePaths.add(path);
    }
  }
   return decodePaths;
}

整个过程涉及到两个关键类LoadPath DecodePath。


LoadPath 由数据类型datacalss 和 DecodePath组成


DecodePath 由数据类型dataclass 解码器 ResourceDecoder 集合 和资源转换 ResourceTranscoder 构成。总体上而言 一个LoadPath的存在代表着可能存在一条路径能够将ModelLoader加载出来的data解码转换成我们指定的数据类型。


DocodeJob数据解码的过程


Glide DecodeJob 的工作过程我们知道SourceGenerator在数据加载完成之后如果允许缓存原始数据会再次执行SourceGenerator#startNext将加载的数据进行缓存,然后通过DataCacheGenerator从缓存文件中获取。最终获取数据成功后会调用DocodeJob#onDataFetcherReady


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