注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

LiveData学习记

LiveData 使用 var liveData: MutableLiveData<String>? = null //初始化 liveData = MutableLiveData() // 设置 observe liveData?.observe...
继续阅读 »

LiveData 使用


var liveData: MutableLiveData<String>? = null
//初始化
liveData = MutableLiveData()
// 设置 observe
liveData?.observe(this, {
Log.e("Main2", "2界面接收数据 = $it")
Toast.makeText(this, "2界面接收数据 = $it", Toast.LENGTH_LONG).show()
})
// 发送值
liveData?.value = "2界面发送数据 $indexValue"

LiveData 是针对同一个界面数据相互传递, 配合 MVVM 使用


如果想跨界面使用 比如 Activity1 想传值 给 Activity2 可以把LiveData 下沉(二次封装)


package com.one_hour.test_livedata
import androidx.lifecycle.MutableLiveData
object LiveDataBusBeta{
//创建一个Map 管理 LiveData
private val liveDataMap: MutableMap<String, MutableLiveData<Any>> = HashMap()
// 设置一个 key
fun <T> getLiveData(key: String) : MutableLiveData<T>? {
if (!liveDataMap.containsKey(key)) {
liveDataMap.put(key, MutableLiveData<Any>())
}
return liveDataMap[key] as MutableLiveData<T>
}
fun removeMapLiveData(key : String) {
liveDataMap.remove(key)
}
}

像这样下沉后会出现 Bug, 如场景:当界面Activity1 向未创建的Activity2 发送消息时,会在Activity2 创建时 出现从界面1传过来的数据,这是我们不需要的。(现象出现叫 消息粘性)


什么是粘性事件

即发射的事件如果早于注册,那么注册之后依然可以接收到的事件称为粘性事件


消息粘性 咋个形成的 ?
先创建 new MutableLiveData -> setValue -> observe(绑定监听)


LiveData 绑定(observe)源码

    @MainThread
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
assertMainThread("observe");
if (owner.getLifecycle().getCurrentState() == DESTROYED) {
// ignore
return;
}
LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
if (existing != null && !existing.isAttachedTo(owner)) {
throw new IllegalArgumentException("Cannot add the same observer"
+ " with different lifecycles");
}
if (existing != null) {
return;
}
owner.getLifecycle().addObserver(wrapper);
}


1.


owner.getLifecycle() 获取的是 Lifecycle 监听Activity 生命周期变化的流程
androidx.appcompat.app.AppCompatActivity (继承)-> androidx.fragment.app.FragmentActivity (继承)-> androidx.activity.ComponentActivity (继承)->androidx.core.app.ComponentActivity( 实现 LifecycleOwner) -> 现在 实例化 private LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);


androidx.core.app.ComponentActivity( 实现 LifecycleOwner)


@CallSuper
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
//添加一个 mLifecycleRegistry 状态管理
mLifecycleRegistry.markState(Lifecycle.State.CREATED);
super.onSaveInstanceState(outState);
}

androidx.activity.ComponentActivity( 实现 LifecycleOwner)


    @CallSuper
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
Lifecycle lifecycle = getLifecycle();
//设置 lifecycle 当前状态
if (lifecycle instanceof LifecycleRegistry) {
((LifecycleRegistry) lifecycle).setCurrentState(Lifecycle.State.CREATED);
}
super.onSaveInstanceState(outState);
mSavedStateRegistryController.performSave(outState);
}

androidx.fragment.app.FragmentActivity


final LifecycleRegistry mFragmentLifecycleRegistry = new LifecycleRegistry(this);
//开始绑定什么周期 调用 handleLifecycleEvent 绑定状态
mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.xxx);

2.


** owner.getLifecycle().addObserver(wrapper); 中 addObserver 调用了 androidx.lifecycle.LifecycleRegistry的 addObserver,而LifecycleRegistry是在FragmentActivity类中实例化获取**


    @Override
public void addObserver(@NonNull LifecycleObserver observer) {
State initialState = mState == DESTROYED ? DESTROYED : INITIALIZED;
ObserverWithState statefulObserver = new ObserverWithState(observer, initialState);
ObserverWithState previous = mObserverMap.putIfAbsent(observer, statefulObserver);

if (previous != null) {
return;
}
LifecycleOwner lifecycleOwner = mLifecycleOwner.get();
if (lifecycleOwner == null) {
// it is null we should be destroyed. Fallback quickly
return;
}

boolean isReentrance = mAddingObserverCounter != 0 || mHandlingEvent;
State targetState = calculateTargetState(observer);
mAddingObserverCounter++;
while ((statefulObserver.mState.compareTo(targetState) < 0
&& mObserverMap.contains(observer))) {
pushParentState(statefulObserver.mState);
statefulObserver.dispatchEvent(lifecycleOwner, upEvent(statefulObserver.mState));
popParentState();
// mState / subling may have been changed recalculate
targetState = calculateTargetState(observer);
}
、、、、、省略代码
}

statefulObserver.dispatchEvent(lifecycleOwner, upEvent(statefulObserver.mState)); 在循环中一直调用


    static class ObserverWithState {
State mState;
LifecycleEventObserver mLifecycleObserver;

ObserverWithState(LifecycleObserver observer, State initialState) {
mLifecycleObserver = Lifecycling.lifecycleEventObserver(observer);
mState = initialState;
}

void dispatchEvent(LifecycleOwner owner, Event event) {
State newState = getStateAfter(event);
mState = min(mState, newState);
mLifecycleObserver.onStateChanged(owner, event);
mState = newState;
}
}

LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer); 监听状态改变 并且
在ObserverWithState 中调用了 mLifecycleObserver.onStateChanged(owner, event); -》mLifecycleObserver 指的就是 LifecycleBoundObserver


class LifecycleBoundObserver extends ObserverWrapper implements GenericLifecycleObserver


        @Override
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
removeObserver(mObserver);
return;
}
// 这里是如果状态 是可见的 那么就发送消息
// 就调用 class LifecycleBoundObserver extends ObserverWrapper 父类 ObserverWrapper 的方法
//shouldBeActive() 获取 mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED); 状态 是否可见
activeStateChanged(shouldBeActive());
}

class LifecycleBoundObserver extends ObserverWrapper 父类 ObserverWrapper 的方法 并分发 dispatchingValue 值


        void activeStateChanged(boolean newActive) {
if (newActive == mActive) {
return;
}
// immediately set active state, so we'd never dispatch anything to inactive
// owner
mActive = newActive;
boolean wasInactive = LiveData.this.mActiveCount == 0;
LiveData.this.mActiveCount += mActive ? 1 : -1;
if (wasInactive && mActive) {
onActive();
}
if (LiveData.this.mActiveCount == 0 && !mActive) {
onInactive();
}
if (mActive) {
// 调用 dispatchingValue 回到 abstract class LiveData<T> 类里面的 dispatchingValue 方法
dispatchingValue(this);
}
}
}

dispatchingValue 都调用了相同的函数 considerNotify


    void dispatchingValue(@Nullable ObserverWrapper initiator) {
if (mDispatchingValue) {
mDispatchInvalidated = true;
return;
}
mDispatchingValue = true;
do {
mDispatchInvalidated = false;
if (initiator != null) {
considerNotify(initiator);
initiator = null;
} else {
for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =
mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
considerNotify(iterator.next().getValue());
if (mDispatchInvalidated) {
break;
}
}
}
} while (mDispatchInvalidated);
mDispatchingValue = false;
}

considerNotify 中 observer.mObserver.onChanged 回调数据


    private void considerNotify(ObserverWrapper observer) {
if (!observer.mActive) {
return;
}
// Check latest state b4 dispatch. Maybe it changed state but we didn't get the event yet.
//
// we still first check observer.active to keep it as the entrance for events. So even if
// the observer moved to an active state, if we've not received that event, we better not
// notify for a more predictable notification order.
if (!observer.shouldBeActive()) {
observer.activeStateChanged(false);
return;
}
if (observer.mLastVersion >= mVersion) {
return;
}
observer.mLastVersion = mVersion;
//noinspection unchecked
observer.mObserver.onChanged((T) mData);
}

解决粘性代码


  • 方法1


import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer


class BaseLiveData<T> : MutableLiveData<T>() {
private var isSticky: Boolean = false
private var mStickyData: T? = null
private var mVersion = 0

override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
if (isSticky) {
super.observe(owner, observer)
} else {
super.observe(owner, CustomObserver<T>(this, observer))
}

}

/**
* 发送非粘性数据
*/
override fun setValue(value: T) {
mVersion++
isSticky = false
super.setValue(value)
}

override fun postValue(value: T) {
mVersion++
isSticky = false
super.postValue(value)
}

/**
* 发送粘性数据
*/
fun setStickyData(data: T?) {
mStickyData = data
isSticky = true
setValue(data!!)
}

fun postStickyData(mStickyData: T?) {
this.mStickyData = mStickyData
isSticky = true
super.postValue(mStickyData!!)
}

inner class CustomObserver<T>(val mLiveData: BaseLiveData<T>, var mObserver: Observer<in T>?,
var isSticky: Boolean = false) : Observer<T> {

private var mLastVersion = mLiveData.mVersion

override fun onChanged(t: T) {
if (mLastVersion >= mLiveData.mVersion) {
if (isSticky && mLiveData.mStickyData != null) {
mObserver?.onChanged(mLiveData.mStickyData)
}
return
}
mLastVersion = mLiveData.mVersion
mObserver?.onChanged(t)

}

}
}


  • 方法2


利用反射 修改 observer.mLastVersion 值
observer.mLastVersion 的 获取值的调用链 :
observer.mLastVersion -》considerNotify (iterator.next().getValue()) -> mObservers (SafeIterableMap<Observer<? super T>, ObserverWrapper> mObservers = new SafeIterableMap<>()) -> ObserverWrapper(int mLastVersion = START_VERSION;) (子类LifecycleBoundObserver, 但是只有父类 ObserverWrapper 才有 mLastVersion, 所以获取父类的 mLastVersion 进行修改)


import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import java.lang.reflect.Field
import java.lang.reflect.Method


class BaseUnStickyLiveData<T> : MutableLiveData<T>() {

private var isSticky = false

override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner, observer)
if (!isSticky) {
hookClass(observer)
}
}

override fun setValue(value: T) {
isSticky = false
super.setValue(value)
}

override fun postValue(value: T) {
isSticky = false
super.postValue(value)
}

fun setStickyValue(value: T) {
isSticky = true
super.setValue(value)
}

fun setStickyPostValue(value: T) {
isSticky = true
super.postValue(value)
}

private fun hookClass(observer: Observer<in T>) {
val liveDataClass = LiveData::class.java
try {
//获取field private SafeIterableMap<Observer<T>, ObserverWrapper> mObservers
val mObservers: Field = liveDataClass.getDeclaredField("mObservers")
mObservers.setAccessible(true)

//获取SafeIterableMap集合mObservers
val observers: Any = mObservers.get(this)

//获取SafeIterableMap的get(Object obj)方法
val observersClass: Class<*> = observers.javaClass
val methodGet: Method = observersClass.getDeclaredMethod("get", Any::class.java)
methodGet.setAccessible(true)

//获取到observer在集合中对应的ObserverWrapper对象
val objectWrapperEntry: Any = methodGet.invoke(observers, observer)
var objectWrapper: Any? = null
if (objectWrapperEntry is Map.Entry<*, *>) {
objectWrapper = objectWrapperEntry.value
}
if (objectWrapper == null) {
//throw NullPointerException("ObserverWrapper can not be null")
return
}

// 获取ListData的mVersion
val mVersion: Field = liveDataClass.getDeclaredField("mVersion")
mVersion.setAccessible(true)
val mVersionValue: Any = mVersion.get(this)

//获取ObserverWrapper的Class对象 LifecycleBoundObserver extends ObserverWrapper
val wrapperClass: Class<*> = objectWrapper.javaClass.superclass

//获取ObserverWrapper的field mLastVersion
val mLastVersion: Field = wrapperClass.getDeclaredField("mLastVersion")
mLastVersion.setAccessible(true)

//把当前ListData的mVersion赋值给 ObserverWrapper的field mLastVersion
mLastVersion.set(objectWrapper, mVersionValue)
} catch (e: Exception) {
if (BuildConfig.DEBUG) {
throw RuntimeException(e)
} else {
e.printStackTrace()
}
}
}
}

配合二次 封装的 LiveDataBusBeta 使用


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

看一遍就理解:动态规划详解

前言 我们刷leetcode的时候,经常会遇到动态规划类型题目。动态规划问题非常非常经典,也很有技巧性,一般大厂都非常喜欢问。今天跟大家一起来学习动态规划的套路,文章如果有不正确的地方,欢迎大家指出哈,感谢感谢~ 什么是动态规划? 动态规划的核心思想 一个例...
继续阅读 »

前言


我们刷leetcode的时候,经常会遇到动态规划类型题目。动态规划问题非常非常经典,也很有技巧性,一般大厂都非常喜欢问。今天跟大家一起来学习动态规划的套路,文章如果有不正确的地方,欢迎大家指出哈,感谢感谢~



  • 什么是动态规划?

  • 动态规划的核心思想

  • 一个例子走进动态规划

  • 动态规划的解题套路

  • leetcode案例分析



公众号:捡田螺的小男孩


什么是动态规划?


动态规划(英语:Dynamic programming,简称 DP),是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题。



dynamic programming is a method for solving a complex problem by breaking it down into a collection of simpler subproblems.



以上定义来自维基百科,看定义感觉还是有点抽象。简单来说,动态规划其实就是,给定一个问题,我们把它拆成一个个子问题,直到子问题可以直接解决。然后呢,把子问题答案保存起来,以减少重复计算。再根据子问题答案反推,得出原问题解的一种方法。



一般这些子问题很相似,可以通过函数关系式递推出来。然后呢,动态规划就致力于解决每个子问题一次,减少重复计算,比如斐波那契数列就可以看做入门级的经典动态规划问题。



动态规划核心思想


动态规划最核心的思想,就在于拆分子问题,记住过往,减少重复计算


动态规划在于记住过往


我们来看下,网上比较流行的一个例子:




  • A : "1+1+1+1+1+1+1+1 =?"

  • A : "上面等式的值是多少"

  • B : 计算 "8"

  • A : 在上面等式的左边写上 "1+" 呢?

  • A : "此时等式的值为多少"

  • B : 很快得出答案 "9"

  • A : "你怎么这么快就知道答案了"

  • A : "只要在8的基础上加1就行了"

  • A : "所以你不用重新计算,因为你记住了第一个等式的值为8!动态规划算法也可以说是 '记住求过的解来节省时间'"



一个例子带你走进动态规划 -- 青蛙跳阶问题


暴力递归



leetcode原题:一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 10 级的台阶总共有多少种跳法。



有些小伙伴第一次见这个题的时候,可能会有点蒙圈,不知道怎么解决。其实可以试想:




  • 要想跳到第10级台阶,要么是先跳到第9级,然后再跳1级台阶上去;要么是先跳到第8级,然后一次迈2级台阶上去。

  • 同理,要想跳到第9级台阶,要么是先跳到第8级,然后再跳1级台阶上去;要么是先跳到第7级,然后一次迈2级台阶上去。

  • 要想跳到第8级台阶,要么是先跳到第7级,然后再跳1级台阶上去;要么是先跳到第6级,然后一次迈2级台阶上去。



假设跳到第n级台阶的跳数我们定义为f(n),很显然就可以得出以下公式:


f(10) = f(9)+f(8)
f (9) = f(8) + f(7)
f (8) = f(7) + f(6)
...
f(3) = f(2) + f(1)

即通用公式为: f(n) = f(n-1) + f(n-2)

那f(2) 或者 f(1) 等于多少呢?



  • 当只有2级台阶时,有两种跳法,第一种是直接跳两级,第二种是先跳一级,然后再跳一级。即f(2) = 2;

  • 当只有1级台阶时,只有一种跳法,即f(1)= 1;


因此可以用递归去解决这个问题:


class Solution {
public int numWays(int n) {
if(n == 1){
return 1;
}
if(n == 2){
return 2;
}
return numWays(n-1) + numWays(n-2);
}
}

去leetcode提交一下,发现有问题,超出时间限制了



为什么超时了呢?递归耗时在哪里呢?先画出递归树看看:




  • 要计算原问题 f(10),就需要先计算出子问题 f(9) 和 f(8)

  • 然后要计算 f(9),又要先算出子问题 f(8) 和 f(7),以此类推。

  • 一直到 f(2) 和 f(1),递归树才终止。


我们先来看看这个递归的时间复杂度吧:


递归时间复杂度 = 解决一个子问题时间*子问题个数


  • 一个子问题时间 = f(n-1)+f(n-2),也就是一个加法的操作,所以复杂度是 O(1);

  • 问题个数 = 递归树节点的总数,递归树的总节点 = 2^n-1,所以是复杂度O(2^n)。


因此,青蛙跳阶,递归解法的时间复杂度 = O(1) * O(2^n) = O(2^n),就是指数级别的,爆炸增长的,如果n比较大的话,超时很正常的了。


回过头来,你仔细观察这颗递归树,你会发现存在大量重复计算,比如f(8)被计算了两次,f(7)被重复计算了3次...所以这个递归算法低效的原因,就是存在大量的重复计算


既然存在大量重复计算,那么我们可以先把计算好的答案存下来,即造一个备忘录,等到下次需要的话,先去备忘录查一下,如果有,就直接取就好了,备忘录没有才开始计算,那就可以省去重新重复计算的耗时啦!这就是带备忘录的解法。


带备忘录的递归解法(自顶向下)


一般使用一个数组或者一个哈希map充当这个备忘录



  • 第一步,f(10)= f(9) + f(8),f(9) 和f(8)都需要计算出来,然后再加到备忘录中,如下:




  • 第二步, f(9) = f(8)+ f(7),f(8)= f(7)+ f(6), 因为 f(8) 已经在备忘录中啦,所以可以省掉,f(7),f(6)都需要计算出来,加到备忘录中~



第三步, f(8) = f(7)+ f(6),发现f(8),f(7),f(6)全部都在备忘录上了,所以都可以剪掉。



所以呢,用了备忘录递归算法,递归树变成光秃秃的树干咯,如下:



备忘录的递归算法,子问题个数=树节点数=n,解决一个子问题还是O(1),所以带备忘录的递归算法的时间复杂度是O(n)。接下来呢,我们用带备忘录的递归算法去撸代码,解决这个青蛙跳阶问题的超时问题咯~,代码如下:


public class Solution {
//使用哈希map,充当备忘录的作用
Map<Integer, Integer> tempMap = new HashMap();
public int numWays(int n) {
// n = 0 也算1种
if (n == 0) {
return 1;
}
if (n <= 2) {
return n;
}
//先判断有没计算过,即看看备忘录有没有
if (tempMap.containsKey(n)) {
//备忘录有,即计算过,直接返回
return tempMap.get(n);
} else {
// 备忘录没有,即没有计算过,执行递归计算,并且把结果保存到备忘录map中,对1000000007取余(这个是leetcode题目规定的)
tempMap.put(n, (numWays(n - 1) + numWays(n - 2)) % 1000000007);
return tempMap.get(n);
}
}
}

去leetcode提交一下,如图,稳了:



其实,还可以用动态规划解决这道题。


自底向上的动态规划


动态规划跟带备忘录的递归解法基本思想是一致的,都是减少重复计算,时间复杂度也都是差不多。但是呢:



  • 带备忘录的递归,是从f(10)往f(1)方向延伸求解的,所以也称为自顶向下的解法。

  • 动态规划从较小问题的解,由交叠性质,逐步决策出较大问题的解,它是从f(1)往f(10)方向,往上推求解,所以称为自底向上的解法。


动态规划有几个典型特征,最优子结构、状态转移方程、边界、重叠子问题。在青蛙跳阶问题中:



  • f(n-1)和f(n-2) 称为 f(n) 的最优子结构

  • f(n)= f(n-1)+f(n-2)就称为状态转移方程

  • f(1) = 1, f(2) = 2 就是边界啦

  • 比如f(10)= f(9)+f(8),f(9) = f(8) + f(7) ,f(8)就是重叠子问题。


我们来看下自底向上的解法,从f(1)往f(10)方向,想想是不是直接一个for循环就可以解决啦,如下:



带备忘录的递归解法,空间复杂度是O(n),但是呢,仔细观察上图,可以发现,f(n)只依赖前面两个数,所以只需要两个变量a和b来存储,就可以满足需求了,因此空间复杂度是O(1)就可以啦



动态规划实现代码如下:


public class Solution {
public int numWays(int n) {
if (n<= 1) {
return 1;
}
if (n == 2) {
return 2;
}
int a = 1;
int b = 2;
int temp = 0;
for (int i = 3; i <= n; i++) {
temp = (a + b)% 1000000007;
a = b;
b = temp;
}
return temp;
}
}

动态规划的解题套路


什么样的问题可以考虑使用动态规划解决呢?



如果一个问题,可以把所有可能的答案穷举出来,并且穷举出来后,发现存在重叠子问题,就可以考虑使用动态规划。



比如一些求最值的场景,如最长递增子序列、最小编辑距离、背包问题、凑零钱问题等等,都是动态规划的经典应用场景。


动态规划的解题思路


动态规划的核心思想就是拆分子问题,记住过往,减少重复计算。 并且动态规划一般都是自底向上的,因此到这里,基于青蛙跳阶问题,我总结了一下我做动态规划的思路:



  • 穷举分析

  • 确定边界

  • 找出规律,确定最优子结构

  • 写出状态转移方程


1. 穷举分析



  • 当台阶数是1的时候,有一种跳法,f(1) =1

  • 当只有2级台阶时,有两种跳法,第一种是直接跳两级,第二种是先跳一级,然后再跳一级。即f(2) = 2;

  • 当台阶是3级时,想跳到第3级台阶,要么是先跳到第2级,然后再跳1级台阶上去,要么是先跳到第 1级,然后一次迈 2 级台阶上去。所以f(3) = f(2) + f(1) =3

  • 当台阶是4级时,想跳到第3级台阶,要么是先跳到第3级,然后再跳1级台阶上去,要么是先跳到第 2级,然后一次迈 2 级台阶上去。所以f(4) = f(3) + f(2) =5

  • 当台阶是5级时......


自底向上的动态规划


2. 确定边界


通过穷举分析,我们发现,当台阶数是1的时候或者2的时候,可以明确知道青蛙跳法。f(1) =1,f(2) = 2,当台阶n>=3时,已经呈现出规律f(3) = f(2) + f(1) =3,因此f(1) =1,f(2) = 2就是青蛙跳阶的边界。


3. 找规律,确定最优子结构


n>=3时,已经呈现出规律 f(n) = f(n-1) + f(n-2) ,因此,f(n-1)和f(n-2) 称为 f(n) 的最优子结构。什么是最优子结构?有这么一个解释:



一道动态规划问题,其实就是一个递推问题。假设当前决策结果是f(n),则最优子结构就是要让 f(n-k) 最优,最优子结构性质就是能让转移到n的状态是最优的,并且与后面的决策没有关系,即让后面的决策安心地使用前面的局部最优解的一种性质



4, 写出状态转移方程


通过前面3步,穷举分析,确定边界,最优子结构,我们就可以得出状态转移方程啦:



5. 代码实现


我们实现代码的时候,一般注意从底往上遍历哈,然后关注下边界情况,空间复杂度,也就差不多啦。动态规划有个框架的,大家实现的时候,可以考虑适当参考一下:


dp[0][0][...] = 边界值
for(状态1 :所有状态1的值){
for(状态2 :所有状态2的值){
for(...){
//状态转移方程
dp[状态1][状态2][...] = 求最值
}
}
}

leetcode案例分析


我们一起来分析一道经典leetcode题目吧



给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。



示例 1:


输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2:


输入:nums = [0,1,0,3,2,3]
输出:4

我们按照以上动态规划的解题思路,



  • 穷举分析

  • 确定边界

  • 找规律,确定最优子结构

  • 状态转移方程


1.穷举分析


因为动态规划,核心思想包括拆分子问题,记住过往,减少重复计算。 所以我们在思考原问题:数组num[i]的最长递增子序列长度时,可以思考下相关子问题,比如原问题是否跟子问题num[i-1]的最长递增子序列长度有关呢?


自顶向上的穷举

这里观察规律,显然是有关系的,我们还是遵循动态规划自底向上的原则,基于示例1的数据,从数组只有一个元素开始分析。



  • 当nums只有一个元素10时,最长递增子序列是[10],长度是1.

  • 当nums需要加入一个元素9时,最长递增子序列是[10]或者[9],长度是1。

  • 当nums再加入一个元素2时,最长递增子序列是[10]或者[9]或者[2],长度是1。

  • 当nums再加入一个元素5时,最长递增子序列是[2,5],长度是2。

  • 当nums再加入一个元素3时,最长递增子序列是[2,5]或者[2,3],长度是2。

  • 当nums再加入一个元素7时,,最长递增子序列是[2,5,7]或者[2,3,7],长度是3。

  • 当nums再加入一个元素101时,最长递增子序列是[2,5,7,101]或者[2,3,7,101],长度是4。

  • 当nums再加入一个元素18时,最长递增子序列是[2,5,7,101]或者[2,3,7,101]或者[2,5,7,18]或者[2,3,7,18],长度是4。

  • 当nums再加入一个元素7时,最长递增子序列是[2,5,7,101]或者[2,3,7,101]或者[2,5,7,18]或者[2,3,7,18],长度是4.


分析找规律,拆分子问题

通过上面分析,我们可以发现一个规律


如果新加入一个元素nums[i], 最长递增子序列要么是以nums[i]结尾的递增子序列,要么就是nums[i-1]的最长递增子序列。看到这个,是不是很开心,nums[i]的最长递增子序列已经跟子问题 nums[i-1]的最长递增子序列有关联了。


原问题数组nums[i]的最长递增子序列 = 子问题数组nums[i-1]的最长递增子序列/nums[i]结尾的最长递增子序列

是不是感觉成功了一半呢?但是如何把nums[i]结尾的递增子序列也转化为对应的子问题呢?要是nums[i]结尾的递增子序列也跟nums[i-1]的最长递增子序列有关就好了。又或者nums[i]结尾的最长递增子序列,跟前面子问题num[j](0=<j<i)结尾的最长递增子序列有关就好了,带着这个想法,我们又回头看看穷举的过程:



nums[i]的最长递增子序列,不就是从以数组num[i]每个元素结尾的最长子序列集合,取元素最多(也就是长度最长)那个嘛,所以原问题,我们转化成求出以数组nums每个元素结尾的最长子序列集合,再取最大值嘛。哈哈,想到这,我们就可以用dp[i]表示以num[i]这个数结尾的最长递增子序列的长度啦,然后再来看看其中的规律:



其实,nums[i]结尾的自增子序列,只要找到比nums[i]小的子序列,加上nums[i] 就可以啦。显然,可能形成多种新的子序列,我们选最长那个,就是dp[i]的值啦




  • nums[3]=5,以5结尾的最长子序列就是[2,5],因为从数组下标0到3遍历,只找到了子序列[2]5小,所以就是[2]+[5]啦,即dp[4]=2

  • nums[4]=3,以3结尾的最长子序列就是[2,3],因为从数组下标0到4遍历,只找到了子序列[2]3小,所以就是[2]+[3]啦,即dp[4]=2

  • nums[5]=7,以7结尾的最长子序列就是[2,5,7][2,3,7],因为从数组下标0到5遍历,找到2,5和3都比7小,所以就有[2,7],[5,7],[3,7],[2,5,7]和[2,3,7]这些子序列,最长子序列就是[2,5,7]和[2,3,7],它俩不就是以5结尾和3结尾的最长递增子序列+[7]来的嘛!所以,dp[5]=3 =dp[3]+1=dp[4]+1



很显然有这个规律:一个以nums[i]结尾的数组nums



  • 如果存在j属于区间[0,i-1],并且num[i]>num[j]的话,则有,dp(i) =max(dp(j))+1,


最简单的边界情况


当nums数组只有一个元素时,最长递增子序列的长度dp(1)=1,当nums数组有两个元素时,dp(2) =2或者1,
因此边界就是dp(1)=1。


确定最优子结构


从穷举分析,我们可以得出,以下的最优结构:


dp(i) =max(dp(j))+1,存在j属于区间[0,i-1],并且num[i]>num[j]。

max(dp(j)) 就是最优子结构。


状态转移方程


通过前面分析,我们就可以得出状态转移方程啦:



所以数组num[i]的最长递增子序列就是:


最长递增子序列 =max(dp[i])

代码实现


class Solution {
public int lengthOfLIS(int[] nums) {
if (nums.length == 0) {
return 0;
}
int[] dp = new int[nums.length];
//初始化就是边界情况
dp[0] = 1;
int maxans = 1;
//自底向上遍历
for (int i = 1; i < nums.length; i++) {
dp[i] = 1;
//从下标0到i遍历
for (int j = 0; j < i; j++) {
//找到前面比nums[i]小的数nums[j],即有dp[i]= dp[j]+1
if (nums[j] < nums[i]) {
//因为会有多个小于nums[i]的数,也就是会存在多种组合了嘛,我们就取最大放到dp[i]
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
//求出dp[i]后,dp最大那个就是nums的最长递增子序列啦
maxans = Math.max(maxans, dp[i]);
}
return maxans;
}
}

参考与感谢



  • leetcode官网

  • 《labuladong算法小抄》

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

如何进一步提高flutter内存表现

前言 性能稳定性是App的生命,Flutter带了很多创新与机遇,然而团队在享受Flutter带来的收益同时也迎接了很多新事物带来的挑战。 本文就内存优化过程中一些实践经验跟大家做一个分享。 Flutter 上线之后 闲鱼使用一套混合栈管理的方案将Flutte...
继续阅读 »

前言


性能稳定性是App的生命,Flutter带了很多创新与机遇,然而团队在享受Flutter带来的收益同时也迎接了很多新事物带来的挑战。


本文就内存优化过程中一些实践经验跟大家做一个分享。


Flutter 上线之后


闲鱼使用一套混合栈管理的方案将Flutter嵌入到现有的App中。在产品体验上我们取得了优于Native的体验。主要得益于Flutter的在跨平台渲染方面的优势,部分原因则是因为我们用Dart语言重新实现的页面抛弃了很多历史的包袱轻装上阵。


上线之后各方面技术指标,都达到甚至超出了部分预期。而我们最为担心的一些稳定性指标,比如crash也在稳定的范围之内。但是在一段时间后我们发现由于内存过高而被系统杀死的abort率数据有比较明显的异常。性能稳定性问题是非常关键的,于是我们火速开展了问题排查。


问题定位与排查


显然问题出在了过大的内存消耗上。内存消耗在App中构成比较复杂,如何在复杂的业务中去定位到罪魁祸首呢?稍加观察,我们确定Flutter问题相对比价明显。工欲善其事必先利其器,需要更好地定位内存的问题,善用已经的工具是非常有帮助的。好在我们在Native层和Dart层都有足够多的性能分析工具进行使用。


工具分析


这里简单介绍我们如何使用的工具去观察手机数据以便于分析问题。需要注意的是,本文的重点不是工具的使用方法介绍,所以只是简单列举部分使用到的常见工具。


Xcode Instruments


Instruments是iOS内存排查的利器,可以比较便捷地观察实时内存使用情况,自然不必多说。


Xcode MemGraph + VMMap


XCode 8之后推出的MEMGraph是Xcode的内存调试利器,可以看到实时的可视化的内存。更为方便的是,你可以将MemGraph导出,配合命令行工具更好的得到结构化的信息。


Dart Observatory


这是Dart语言官方的调试工具,里面也包含了类似于Xcode的Instruments的工具。在Debug模式下Dart VM启动以后会在特定的端口接受调试请求。官方文档


观察结果


在整个过程中我进行了大量的观察,这里分享一部分典型的数据表现。


通过Xcode Instruments排查的话,我们观察到CG Raster Data这个数据有些高。这个Raster Data呢其实是图片光栅化的时候的内存消耗。


我们将App内存异常的场景的MemGraph导出来,对其执行VMMap指令得出的结果:


vmmap --summary Runner[40957].memgraph

vmmap Runner[40957].memgraph | grep 'IOKit'

vmmap Summary


vmmap address


我们主要关注resident和dirty的内存。发现IOKit占用了大量的内存。


结合Xcode Raster Data还有IOKit的大量内存消耗,我们开始怀疑问题是图内存泄漏导致的。经过进一步通过Dart Observatory观察Dart Image对象的内存情况。

Dart image instance

观察结果显示,在内存较高的场景下在Dart层的确同时存在了较多Image(如图中270)的对象。现在基本可以确定内存问题跟Dart层的图片有很大的关系。


这个结果,我估计很多人都已经想到了,App有明显的内存问题很有可能就是跟多媒体资源有关系。通过工具得出的准确数据线索,我们得到一个大致的方向去深入研究。


诡异的Dart图片数量爆炸


图片对象泄漏?


前面我们用工具观察到Dart层的Image对象数量过多直接导致了非常大的内存压力,我们起初怀疑存在图片的内存泄漏。但是我们在经过进一步确认以后发现图片其实并没有真正的泄漏。


Dart语言采用垃圾回收机制(Garbage Collection 下面开始简称GC)来管理分配的内存,VM层面的垃圾回收应该大多数情况下是可信的。但是从实际观察来看,图片数量的爆炸造成的较大的内存峰值直观感觉上GC来得有些不及时。在Debug模式下我们使用Dart Observatory手动触发GC,最终这些图片对象在没有引用的情况下最终还是会被回收。


至此,我们基本可以确认,图片对象不存在泄漏。那是什么导致了GC的反应迟钝呢,难道是Dart语言本身的问题吗?


Garbage Collection 不及时?


为此我需要了解一下Dart内存管理机制垃圾回收的实现,关于详细的内存问题我团队的 @匠修 同学已经发过一篇相关文章可以参考:内存文章


我这里不详细讨论Dart垃圾回收实现细节,只聊一聊Flutter与Dart相关的一些内容。


关于Flutter我需要首先明确几个概念:




  1. Framework(Dart)(跟iOS平台连接的库Flutter.framework要区别开)特指由Dart编写的Flutter相关代码。




  2. Dart VM执行Dart代码的Dart语言相关库,它是以C实现的Dart SDk形式提供的。对外主要暴露了C接口Dart Api。里面主要包含了Dart的编译器,运行时等等。




  3. FLutter Engine C++实现的Flutter驱动引擎。他主要负责跨平台的绘制实现,包含Skia渲染引擎的接入;Dart语言的集成;以及跟Native层的适配和Embeder相关的一些代码。简单理解,iOS平台上面Flutter.framework, Android平台上的Flutter.jar便是引擎代码构建后的产物。




在Dart代码里面对于GC是没有感知的。


对于Dart SDK也就是Dart语言我们可以做的很有限,因为Dart语言本身是一种标准,如果Dart真的有问题我们需要和Dart维护团队协作推进问题的解决。Dart语言设计的时候初衷也是希望GC对于使用者是透明的,我们不应该依赖GC实现的具体算法和策略。不过我们还是需要通过Dart SDK的源码去理解GC的大致情况。


既然我们前面已经确认并非内存泄漏,所以我们在对GC延迟的问题的调查主要放在Flutter Engine以及Dart CG入口上。


Flutter与Dart Garbage Collection


既然感觉GC不及时,先撇开消耗,我们至少可以尝试多触发几次GC来减轻内存峰值压力。但是我在仔细查阅dart_api.h(/src/third_party/dart/runtime/include/dart_api.h )接口文件后,但是并没有找到显式提供触发GC的接口。


但是找到了如下这个方法Dart_NotifyIdle


/**
* Notifies the VM that the embedder expects to be idle until |deadline|. The VM
* may use this time to perform garbage collection or other tasks to avoid
* delays during execution of Dart code in the future.
*
* |deadline| is measured in microseconds against the system's monotonic time.
* This clock can be accessed via Dart_TimelineGetMicros().
*
* Requires there to be a current isolate.
*/

DART_EXPORT void Dart_NotifyIdle(int64_t deadline);

这个接口意思是我们可以在空闲的时候显式地通知Dart,你接下来可以利用这些时间(dealine之前)去做GC。注意,这里的GC不保证会马上执行,可以理解我们请求Dart去做GC,具体做不做还是取决于Dart本身的策略。


另外,我还找到一个方法叫做Dart_NotifyLowMemory:


/**
* Notifies the VM that the system is running low on memory.
*
* Does not require a current isolate. Only valid after calling Dart_Initialize.
*/

DART_EXPORT void Dart_NotifyLowMemory();

不过这个Dart_NotifyLowMemory方法其实跟GC没有太大关系,它其实是在低内存的情况下把多余的isolate去终止掉。你可以简单理解,把一些不是必须的线程给清理掉。


在研究Flutter Engine代码后你会发现,Flutter Engine其实就是通过Dart_NotifyIdle去跟Dart层进行GC方面的协作的。我们可以在Flutter Engine源码animator.cc看到以下代码:


  
//Animator负责刷新和通知帧的绘制
if (!frame_scheduled_) {
// We don't have another frame pending, so we're waiting on user input
// or I/O. Allow the Dart VM 100 ms.
delegate_.OnAnimatorNotifyIdle(*this, dart_frame_deadline_ + 100000);
}


//delegate 最终会调用到这里
bool RuntimeController::NotifyIdle(int64_t deadline) {
if (!root_isolate_) {
return false;
}

tonic::DartState::Scope scope(root_isolate_.get());
//Dart api接口
Dart_NotifyIdle(deadline);
return true;
}

这里的逻辑比较直观:如果当前没有帧渲染的任务时候就通过NotifyIdle告诉Dart层可以进行GC操作了。注意,这里并不是说只有在这种情况下Dart才回去做GC,Flutter只是通过这种方式尽可能利用空闲去做GC,配合Dart以更合理的时间去做GC。


看到这里,我们有足够的理由去尝试一下这个接口,于是我们在一些内存压力比较大的场景进行了手动请求GC的操作。线上的Abort虽然有明显好转,但是内存峰值并没有因此得到改善。我们需要进一步找到根本原因。


图片数量爆炸的真相


为了确定图片大量囤积释放不及时的问题,我们需要跟踪Flutter图片从初始化到销毁的整个流程。


我们从Dart层开始去追寻Image对象的生命周期,我们可以看到Flutter里面所以的图片都是经过ImageProvider来获取的,ImageProvider在获取图片的时候会调用一个Resolve接口,而这个接口会首先查询ImageCache去读取图片,如果不存在缓存就new Image的实例出来。


关键代码:


  ImageStream resolve(ImageConfiguration configuration) {
assert(configuration != null);
final ImageStream stream = new ImageStream();
T obtainedKey;
obtainKey(configuration).then((T key) {
obtainedKey = key;
stream.setCompleter(PaintingBinding.instance.imageCache.putIfAbsent(key, () => load(key)));
}).catchError(
(dynamic exception, StackTrace stack) async {
FlutterError.reportError(new FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'services library',
context: 'while resolving an image',
silent: true, // could be a network error or whatnot
informationCollector: (StringBuffer information) {
information.writeln('Image provider: $this');
information.writeln('Image configuration: $configuration');
if (obtainedKey != null)
information.writeln('Image key: $obtainedKey');
}
));
return null;
}
);
return stream;
}

大致的逻辑



  1. Resolve 请求获取图片.

  2. 查询是否存在于ImageCache.Yes->3 NO->4

  3. 返回已经存在的图片对象

  4. 生成新的Image对象并开始加载
    看起来没有特别复杂的逻辑,不过这里我要提一下Flutter ImageCache的实现。


Flutter ImageCache


Flutter ImageCache最初的版本其实非常简单,用Map实现的基于LRU算法缓存。这个算法和实现没有什么问题,但是要注意的是ImageCache缓存的是ImageStream对象,也就是缓存的是一个异步加载的图片的对象。而且缓存没有对占用内存总量做限制,而是采用默认最大限制1000个对象(Flutter在0.5.6 beta中加入了对内存大小限制的逻辑)。缓存异步加载对象的一个问题是,在图片加载解码完成之前,无法知道到底将要消耗多少内存,至少在Flutter这个Cache实现中没有处理这个问题。具体的实现感兴趣的朋友可以阅读ImageCache.dart源码。


其实Flutter本身提供了定制化Cache的能力,所以优化ImageCache的第一步就是要根据机型的物理内存去做缓存大小的适配,设置ImageCache的合理限制。关于ImageCache的问题,可以参考官方文档和这个issue,我这里不展开去聊了。


Flutter Image生命周期


回到我们的Image对象跟踪,很明显,在缓存没有命中的情况下会有新的Image产生。继续深入代码会发现Image对象是由这段代码产生的:



Future instantiateImageCodec(Uint8List list) {
return _futurize(
(_Callback callback) => _instantiateImageCodec(list, callback, null)
);
}

String _instantiateImageCodec(Uint8List list, _Callback callback, _ImageInfo imageInfo)
native 'instantiateImageCodec';

这里有个native关键字,这是Dart调用C代码的能力,我们查看具体的源码可以发现这个最终初始化的是一个C++的codec对象。具体的代码在Flutter Engine codec.cc。它大致的过程就是先在IO线程中启动了一个解码任务,在IO完成之后再把最终的图片对象发回UI线程。关于Flutter线程的详细介绍,我在另外一篇文章中已经有介绍,这里附上链接给有兴趣的朋友。深入理解Flutter Engine线程模型。经过来这些代码和线程分析,我们得到大致的流程图:


图片爆炸流程图


也就是说,解码任务在IO线程进行,IO任务队列里面都是C++ lambda表达式,持有了实际的解码对象,也就持有了内存资源。当IO线程任务过多的时候,会有很多IO任务在等待执行,这些内存资源也被闭包所持有而等待释放。这就是为什么直观上会有内存释放不及时而造成内存峰值的问题。这也解释了为什么之前拿到的vmmap虚拟内存数据里面IOKit是大头。


这样我们找到了关键的线索,在缓存不命中的情况下,大量初始化Image对象,导致IO线程任务繁重,而IO又持有大量的图片解码所用的内存资源。带这个推论,我在Flutter Engine的Task Runner加入了任务数量和C++ image对象的监控代码,证实了的确存在IO任务线程过载的情况,峰值在极端情况下瞬时达到了100+IO操作。


IO Runner监控


到这里问题似乎越来越明了了,但是为什么会有这么IO任务触发呢?上述逻辑虽然可能会有IO线程过载的情况下占用大量内存的情况。上层要求生成新的图片对象,这种请求是没有错误的,设计就是如此。就好比主线程阻塞大量的任务,必然会导致界面卡顿,但者却不是主线程本身的问题。我们需要从源头找到导致新对象创建暴涨真正导致IO线程过载的原因。


大量请求的根源


在前面的线索之下,我们继续寻找问题的根源。我们在实际App操作的过程当中发现,页面Push的越多,图片生成的速度越来越快。也就是说页面越多请求越快,看起来没有什么大问题。但是可见的图片其实总是在一定数量范围之内的,不应该随着页面增多而加快对象创建的频率。我们下意识的开始怀疑是否存在不可见的Image Widget也在不断请求图片的情况。最终导致了Cache无法命中而大量生成新的图片的场景。


我开始调查每个页面的图片加载请求,我们知道Flutter里面万物皆Widget,页面都是是Widget,由Navigator管理。我在Widget的生命周期方法(详细见Flutter官方文档)中加入监控代码,如我所料,在Navigator栈底下不可见的页面也还在不停的Resolve Image,直接导致了image对象暴涨而导致IO线程过载,导致了内存峰值。


看起来,我们终于找到了根本原因。解决方案并不难。在页面不可见的时候没必要发出多余的图片加载请求,峰值也就随之降下来了。再经过一番代码优化和测试以后问题得到了根本上的解决。优化上线以后,我们看到了数据发生了质的好转。
有朋友可能想问,为什么不可见的Widget也会被调用到相关的生命周期方法。这里我推荐阅读Flutter官方文档关于Widget相关的介绍,篇幅有限我这里不展开介绍了。widgets


至此,我们已经解决了一个较为严重的内存问题。内存优化情况复杂,可以点也比较多,接下来我继续简要分享在其它一些方面的优化方案。


截图缓存优化


文件缓存+预加载策略


我们是采用嵌入式Flutter并使用一套混合栈模式管理Native和Flutter页面相互跳转的逻辑。由于FlutterView在App中是单例形式存在的,我们为了更好的用户体验,在页面切换的过程中使用的截图的方式来进行过渡。


大家都知道,图片是非常占用内存的对象,我们如何在不降低用户体验的同时获得最小的内存消耗呢?假如我们每push一个页面都保存一张截图,那么内存是以线性复杂度增长的,这显然不够好。


内存和空间在大多数情况下是一个互相转换的关系,优化很多时候其实是找一个合理的折中点。
最终我采用了预加载+缓存的策略,在页面最多只在内存中同时存在两个截图,其它的存文件,在需要的时候提前进行预加载。
简要流程图:


简要流程图


这样的话就做到了不影响用户体验的前提下,将空间复杂度从O(n)降低到了O(1)。
这个优化进一步节省了不必要的内存开销。


截图额外的优化



  • 针对当前设备的内存情况,自适应调整截图的分辨率,争取最小的内存消耗。

  • 在极端的内存情况下,把所有截图都从内存中移除存(存文件可恢复),采用PlaceHolder的形式。极端情况下避免被杀,保证可用性的体验降级策略。


页面兜底策略


对于电商类App存在一个普遍的问题,用户会不断的push页面到栈里面,我们不能阻止用户这种行为。我们当然可以把老页面干掉,每次回退的时候重新加载,但是这种用户体验跟Web页一样,是用户不可接受的。我们要维持页面的状态以保证用户体验。这必然会导致内存的线性增长,最终肯定难免要被杀。我们优化的目的是提高用户能够push的极限页面数量。


对于Flutter页面优化,除了在优化每一个页面消耗的内存之外,我们做了降级兜底策略去保证App的可用性:在极端情况下将老页面进行销毁,在需要的时候重新创建。这的确降低了用户体验,在极端情况下,降级体验还是比Crash要好一些。



FlutterViewController 单例析构


另外我想讨论的一个话题是关于FlutterViewController的。目前Flutter的设计是按照单例模式去运行的,这对于完全用Flutterc重新开发的App没有太大的问题。但是对于混合型App,多出来的常驻内存确实是一个问题。


实际上,Flutter Engine底层实现是考虑到了析构这个问题,有相关的接口。但是在Embeder这一层(具体FlutterViewController Message Channels这一层),在实现过程中存在一些循环引用,导致在Native层就算没有引用FlutterViewController的时候也无法释放.


FlutterViewController引用图


我在经过一段时间的尝试后,算是把循环引用解除了。这些循环引用主要集中在FlutterChannel这一块。在解除之后我顺利的释放了FlutterViewController,可以明显看到常驻内存得到了释放。但是我发现释放FlutterViewController的时候会导致一部分Skia Image对象泄漏,因为Skia Objects必须在它创建的线程进行释放(详情请参考skia_gpu_object.cc源码),线程同步的问题。关于这个问题我在GitHub上面有一个issue大家可以参考。FlutterViewController释放issue


目前,这个优化我们已经反馈给Flutter团队,期待他们官方支持。希望大家可以一起探索研究。


进一步探讨


除此之外,Flutter内存方面其实还有比较多方面可以去研究。我这里列举几个目前观察到的问题。




  1. 我在内存分析的时候发现Flutter底层使用的boring ssl库有可以确定的内存泄漏。虽然这个泄漏比较缓慢,但是对于App长期运行还是有影响的。我在GitHub上面提了个issue跟进,目前已有相关的人员进行跟进。SSL leak issue




  2. 关于图片渲染,目前Flutter还是有优化空间的,特别是图片的按需剪裁。大多数情况下是没有不要将整一个bitmap解压到内存中的,我们可以针对显示的区域大小和屏幕的分辨率对图片进行合理的缩放以取得最好的性能消耗。




  3. 在分析Flutter内存的MemGraph的时候,我发现Skia引擎当中对于TextLayout消耗了大量的内存.目前我没有找到具体的原因,可能存在优化的空间。




结语


在这篇文章里,我简要的聊了一下目前团队在Flutter应用内存方面做出的尝试和探索。短短一篇文章无法包含所有内容,只能推出了几个典型的案例来作分析,希望可以跟大家一起探讨研究。欢迎感兴趣的朋友一起研究,如有更好的想法方案,我非常乐意看到你的分享。


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

Flutter动画实现粒子漂浮效果

要问2019年最火的移动端框架,肯定非Google的Flutter莫属。 本着学习的态度,基本的Dart语法(个人感觉语法风格接近Java+JS)过完之后,开始撸代码练手。 效果图 (这里为了方便录制gif,动画设置的较快;如果将动画的Duration设...
继续阅读 »

要问2019年最火的移动端框架,肯定非Google的Flutter莫属。

image

本着学习的态度,基本的Dart语法(个人感觉语法风格接近Java+JS)过完之后,开始撸代码练手。




效果图


image


(这里为了方便录制gif,动画设置的较快;如果将动画的Duration设置成20s,看起来就是浮动的效果了)
粒子碰撞的效果参考了张风捷特列 大佬的Flutter动画之粒子精讲


1. Flutter的动画原理



在任何系统的UI框架中,动画实现的原理都是相同的,即:在一段时间内,快速地多次改变UI外观;由于人眼会产生视觉暂留,所以最终看到的就是一个“连续”的动画,这和电影的原理是一样的。我们将UI的一次改变称为一个动画帧,对应一次屏幕刷新,而决定动画流畅度的一个重要指标就是帧率FPS(Frame Per Second),即每秒的动画帧数。



简而言之,就是逐帧绘制,只要屏幕刷新的足够快,我们就会觉得这是个连续的动画。
设想一个小球从屏幕顶端移动到底端的动画,为了完成这个动画,我们需要哪些数据呢?



  • 小球的运动轨迹,即起始点s、终点e和中间任意一点p

  • 动画持续时长t


只有这两个参数够吗?明显是不够的,因为小球按照给定的轨迹运动,可能是匀速、先快后慢、先慢后快、甚至是一会儿快一会慢的交替地运动,只要在时间t内完成,都是可能的。所以我们应该再指定一个参数c来控制动画的速度。


1.1 vsync探究


废话不多说,我们看看Flutter中是动画部分的代码:


AnimationController controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
)..addListener(() {
//_renderBezier();
print(controllerG.value);
print('这是第${++count}次回调');
});
复制代码

简要分析一下,AnimationController,顾名思义,控制器,用来控制动画的播放。传入的参数中,duration我们知道是前面提到过的动画持续时长t,那这个vsync是啥参数呢?打过游戏的同学可能对这个单词有印象,vsync 就是 垂直同步 。那什么是垂直同步呢?



垂直同步又称场同步(Vertical Hold),从CRT显示器的显示原理来看,单个像素组成了水平扫描线,水平扫描线在垂直方向的堆积形成了完整的画面。显示器的刷新率受显卡DAC控制,显卡DAC完成一帧的扫描后就会产生一个垂直同步信号。我们平时所说的打开垂直同步指的是将该信号送入显卡3D图形处理部分,从而让显卡在生成3D图形时受垂直同步信号的制约。



简而言之就是,显卡在完成渲染后,将画面数据送往显存中,而显示器从显存中一行一行从上到下取出画面数据,进行显示。但是屏幕的刷新率和显卡渲染数据的速度很多时候是不匹配的,试想一下,显示器刚扫描显示完屏幕上半部分的画面,正准备从显存取下面的画面数据时,显卡送来了下一帧的图像数据覆盖了原来的显存,这个时候显示器取出的下面部分的图像就和上面的不匹配,造成画面撕裂。


为了避免这种情况,我们引入垂直同步信号,只有在显示器完整的扫描显示完一帧的画面后,显卡收到垂直同步信号才能刷新显存。
可是这个物理信号跟我们flutter动画有啥关系呢?vsync对应的参数是this,我们继续分析一下this对应的下面的类。


class _RunBallState extends State<RunBall> with TickerProviderStateMixin 
复制代码

with关键字是使用该类的方法而不继承该类,Mixin是类似于Java的接口,区别在于Mixin中的方法不是抽象方法而是已经实现了的方法。



这个TickerProviderStateMixin到底是干啥的呢???经过哥们儿Google的帮助,在网上找到了



关于动画的驱动,在此简单的说一下,Ticker是被SchedulerBinding所驱动。SchedulerBinding则是监听着Window.onBeginFrame回调。
Window.onBeginFrame的作用是什么呢,是告诉应用该提供一个scene了,它是被硬件的VSync信号所驱动的。



于是我们终于发现了,绕了一圈,归根到底还是真正的硬件产生的垂直同步信号在驱动着Flutter的动画的进行。


..addListener(() {
//_renderBezier();
print(controllerG.value);
print('这是第${++count}次回调');
});

复制代码

注意到之前的代码中存在一个动画控制器的监听器,动画在执行时间内,函数回调controller.value会生成一个从0到1的double类型的数值。我们在控制台打印出结果如下:

image


image


经过观察,两次试验,在2s的动画执行时间内,该回调函数分别被执行了50次,53次,并不是一个固定值。也就是说硬件(模拟器)的屏幕刷新率大概维持在(25~26.5帧/s)。


结论:硬件决定动画刷新率


1.2 动画动起来


搞懂了动画的原理之后,我们接下来就是逐帧的绘制了。关于Flutter的自定义View,跟android原生比较像。


image


继承CustomPainter类,重写paint和shouldRepaint方法,具体实现可以看代码.


class Ball {
double aX;
double aY;
double vX;
double vY;
double x;
double y;
double r;
Color color;}

复制代码

小球Ball具有圆心坐标、半径、颜色、速度、加速度等属性,通过数学表达式计算速度和加速度的关系,就可以实现匀加速的效果。


//运动学公式,看起来少了个时间t;实际上这些函数在动画过程中逐帧回调,把每帧刷新周期当成单位时间,相当于t=1
_ball.x += _ball.vX;//位移=速度*时间
_ball.y += _ball.vY;
_ball.vX += _ball.aX;//速度=加速度*时间
_ball.vY += _ball.aY;

复制代码

控制器使得函数不断回调,在回调函数函数里改变小球的相关参数,并且调用setState()函数,使得UI重新绘制;小球的轨迹坐标不断地变化,逐帧绘制的小球看起来就在运动了。你甚至可以在添加效果使得小球在撞到边界时变色或者半径变小(参考文章开头的粒子碰撞效果图)。


2. 小球随机浮动的思考


问题来了,我想要一个漂浮的效果呢?最好是随机的轨迹,就像气泡在空中飘乎不定,于是引起了我的思考;匀速然后方向随机?感觉不够优雅,于是去网上搜了一下,发现了思路!



首先随机生成一条贝塞尔曲线作为轨迹,等小球运动到终点,再生成新的贝塞尔曲线轨迹



生成二阶贝塞尔曲线的公式如下:


//二次贝塞尔曲线轨迹坐标,根据参数t返回坐标;起始点p0,控制点p1,终点p2
Offset quadBezierTrack(double t, Offset p0, Offset p1, Offset p2) {
var bx = (1 - t) * (1 - t) * p0.dx + 2 * t * (1 - t) * p1.dx + t * t * p2.dx;
var by = (1 - t) * (1 - t) * p0.dy + 2 * t * (1 - t) * p1.dy + t * t * p2.dy;

return Offset(bx, by);
}
复制代码

很巧的是,这里需要传入一个0~1之间double类型的参数t,恰好前面我们提过,animationController会在给定的时间内,生成一个0~1的value;这太巧了。


起始点的坐标不用说,接下来就剩解决控制点p1和p2,当然是随机生成这两点,但是如果同时有多个小球呢?比如5个小球同时进行漂浮,每个小球都对应一组三个坐标的信息,给小球Ball添加三个坐标的属性?不,这个时候,我们可以巧妙地利用带种子参数的随机数。



我们知道随机数在生成的时候,如果种子相同的话,每次生成的随机数也是相同的。



每个小球对象在创建的时候自增地赋予一个整形的id,作为随机种子;比如5个小球,我们起始的id为:2,4,6,8,10;


    Offset p0 = ball.p0;//起点坐标
Offset p1 = _randPosition(ball.id);
Offset p2 = _randPosition(ball.id + 1);
复制代码

rand(2),rand(2+1)为第一个小球的p1和p2坐标;当所有小球到达终点时,此时原来的终点p2为新一轮贝塞尔曲线的起点;此时相应的id也应增加,为了防止重复,id应增加小球数量5 *2,即第二轮运动开始时,5个小球的id为:12,14,16,18,20。
这样就保证了每轮贝塞尔曲线运动的时候,对于每个小球而言,p0,p1,p2是确定的;新一轮的运动所需要的随机的三个坐标点,只需要改变id的值就好了。


Path quadBezierPath(Offset p0, Offset p1, Offset p2) {
Path path = new Path();
path.moveTo(p0.dx, p0.dy);
path.quadraticBezierTo(p1.dx, p1.dy, p2.dx, p2.dy);
return path;
}
复制代码

这个时候,我们还可以利用Flutter自带的api画出二次贝塞尔曲线的轨迹,看看小球的运动是否落在轨迹上。


image


2.1 一些细节


animation = CurvedAnimation(parent: controllerG, curve: Curves.bounceOut);
复制代码

这里的Curve就是前面提到的,控制动画过程的参数,flutter自带了挺多效果,我最喜欢这个bounceOut(弹出效果)

image


 animation.addStatusListener((status) {
switch (status) {
case AnimationStatus.dismissed:
// TODO: Handle this case.
break;
case AnimationStatus.forward:
// TODO: Handle this case.
break;
case AnimationStatus.reverse:
// TODO: Handle this case.
break;
case AnimationStatus.completed:
// TODO: Handle this case.
controllerG.reset();
controllerG.forward();
break;
}
});

复制代码

监听动画过程的状态,当一轮动画结束时,status状态为AnimationStatus.completed;此时,我们将控制器reset重置,再forward重新启动,此时就会开始新一轮的动画效果;如果我们选的是reverse,则动画会反向播放。




GestureDetector(
child: Container(
width: double.infinity,
height: 200,
child: CustomPaint(
painter: FloatBallView(_ballsF, _areaF),
),
),
onTap: () {
controllerG.forward();
},
onDoubleTap: () {
controllerG.stop();
},
),
复制代码

为了方便控制,我还加了个手势监听器,单击控制动画运行,双击暂停动画。


3 完结


水平有限,文中如有错误还请各位指出,我是梦龙Dragon


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

浅探Google V8引擎

探析它之前,我们先抛出以下几个疑问:为什么需要 V8 引擎呢?V8 引擎到底是个啥?它可以做些什么呢?了解它能有什么收获呢?接下来就针对以上几个问题进行详细描述。由来我们都知道,JS 是一种解释型语言,支持动态类型(明显不同于 Java 等这类静态语言就是在命...
继续阅读 »

探析它之前,我们先抛出以下几个疑问:

  • 为什么需要 V8 引擎呢?

  • V8 引擎到底是个啥?

  • 它可以做些什么呢?

  • 了解它能有什么收获呢?

接下来就针对以上几个问题进行详细描述。

由来

我们都知道,JS 是一种解释型语言,支持动态类型(明显不同于 Java 等这类静态语言就是在命名一个变量时不需要声明变量的类型)、弱类型、基于原型的语言,内置支持类型。而一般 JS 都是在前端执行(直接影响界面),需要能够快速响应用户,那么就要求语言本身可以被快速地解析和执行,JS 引擎就为此而问世。

这里提到了解释型语言和静态语言(编译型语言),先简单介绍一下二者:

  • 解释型语言(JS)

    • 每次运行时需要解释器按句依次解释源代码执行,将它们翻译成机器认识的机器代码再执行

  • 编译型语言(Java)

    • 运行时可经过编译器翻译成可执行文件,再由机器运行该可执行文件即可

从上面的描述中可以看到 JS 运行时每次都要根据源文件进行解释然后执行,而编译型的只需要编译一次,下次可直接运行其可执行文件,但是这样就会导致跨平台的兼容性很差,因此各有优劣。

而众多 JS 引擎(V8、JavaScriptCore、SpiderMonkey、Chakra等)中 V8 是最为出色的,加上它也是应用于当前最流行的谷歌浏览器,所以我们非常有必要去认识和了解一下,这样对于开发者也就更清楚 JS 在浏览器中到底是如何运行的了。

认识

定义

  • 使用 C++ 开发

  • 谷歌开源

  • 编译成原生机器码(支持IA-32, x86-64, ARM, or MIPS CPUs)

  • 使用了如内联缓存(inline caching)等方法来提高性能

  • 运行速度快,可媲美二进制程序

  • 支持众多操作系统,如 windows、linux、android 等

  • 支持其他硬件架构,如 IA32,X64,ARM 等

  • 具有很好的可移植和跨平台特性

运行

先来一张官方流程图:

img

准备

JS 文件加载(不归 V8 管):可能来自于网络请求、本地的cache或者是也可以是来自service worker,这是 V8 运行的前提(有源文件才有要解释执行的)。 3种加载方式 & V8的优化

  • Cold load: 首次加载脚本文件时,没有任何数据缓存

  • Warm load:V8 分析到如果使用了相同的脚本文件,会将编译后的代码与脚本文件一起缓存到磁盘缓存中

  • Hot load: 当第三次加载相同的脚本文件时,V8 可以从磁盘缓存中载入脚本,并且还能拿到上次加载时编译后的代码,这样可以避免完全从头开始解析和编译脚本

而在 V8 6.6 版本的时候进一步改进代码缓存策略,简单讲就是从缓存代码依赖编译过程的模式,改变成两个过程解耦,并增加了可缓存的代码量,从而提升了解析和编译的时间,大大提升了性能,具体细节见V8 6.6 进一步改进缓存性能

分析

此过程是将上面环节得到的 JS 代码转换为 AST(抽象语法树)。

词法分析

从左往右逐个字符地扫描源代码,通过分析,产生一个不同的标记,这里的标记称为 token,代表着源代码的最小单位,通俗讲就是将一段代码拆分成最小的不可再拆分的单元,这个过程称为词法标记,该过程的产物供下面的语法分析环节使用。

这里罗列一下词法分析器常用的 token 标记种类:

  • 常数(整数、小数、字符、字符串等)

  • 操作符(算术操作符、比较操作符、逻辑操作符)

  • 分隔符(逗号、分号、括号等)

  • 保留字

  • 标识符(变量名、函数名、类名等)

TOKEN-TYPE TOKEN-VALUE\
-----------------------------------------------\
T_IF                 if\
T_WHILE              while\
T_ASSIGN             =\
T_GREATTHAN          >\
T_GREATEQUAL         >=\
T_IDENTIFIER name    / numTickets / ...\
T_INTEGERCONSTANT    100 / 1 / 12 / ....\
T_STRINGCONSTANT     "This is a string" / "hello" / ...

上面提到会逐个从左至右扫描代码然后分析,那么很明显就会想到两种方案,扫描完再分析(非流式处理)和边扫描边分析(流式处理),简单画一下他们的时序图就能发现流式处理效率要高得多,同时分析完也会释放分析过程中占用的内存,也能大大提高内存使用效率,可见该优化的细节处理。

语法分析

语法分析是指根据某种给定的形式文法对由单词序列构成的输入文本(例如上个阶段的词法分析产物-tokens stream),进行分析并确定其语法结构的过程,最后产出其 AST(抽象语法树)。

V8 会将语法分析的过程分为两个阶段来执行:

  • Pre-parser

    • 跳过还未使用的代码

    • 不会生成对应的 AST,会产生不带有变量的引用和声明的 scopes 信息

    • 解析速度会是 Full-parser 的 2 倍

    • 根据 JS 的语法规则仅抛出一些特定的错误信息

  • Full-parser

    • 解析那些使用的代码

    • 生成对应的 AST

    • 产生具体的 scopes 信息,带有变量引用和声明等信息

    • 抛出所有的 JS 语法错误

为什么要做两次解析?

如果仅有一次,那只能是 Full-parser,但这样的话,大量未使用的代码会消耗非常多的解析时间,结合实例来看下:通过 Coverage 录制的方式可以分析页面哪些代码没有用到,如下图可以看到最高有 75% 的没有被执行。

img

但是预解析并不是万能的,得失是并存的,很明显的一个场景:该文件中的代码全都执行了,那其实就是没必要的,当然这种情况其实还是占比远不如上面的例子,所以这里其实也是一种权衡,需要照顾大多数来达到综合性能的提升。

下面给出一个示例:

function add(x, y) {
   if (typeof x === "number") {
       return x + y;
  } else {
       return x + 'tadm';
  }
}

复制上面的代码到 web1web2 可以很直观的看到他们的 tokens 和 AST 结构(也可自行写一些代码体验)。

img

  • tokens

[
  {
       "type": "Keyword",
       "value": "function"
  },
  {
       "type": "Identifier",
       "value": "add"
  },
  {
       "type": "Punctuator",
       "value": "("
  },
  {
       "type": "Identifier",
       "value": "x"
  },
  {
       "type": "Punctuator",
       "value": ","
  },
  {
       "type": "Identifier",
       "value": "y"
  },
  {
       "type": "Punctuator",
       "value": ")"
  },
  {
       "type": "Punctuator",
       "value": "{"
  },
  {
       "type": "Keyword",
       "value": "if"
  },
  {
       "type": "Punctuator",
       "value": "("
  },
  {
       "type": "Keyword",
       "value": "typeof"
  },
  {
       "type": "Identifier",
       "value": "x"
  },
  {
       "type": "Punctuator",
       "value": "==="
  },
  {
       "type": "String",
       "value": "\"number\""
  },
  {
       "type": "Punctuator",
       "value": ")"
  },
  {
       "type": "Punctuator",
       "value": "{"
  },
  {
       "type": "Keyword",
       "value": "return"
  },
  {
       "type": "Identifier",
       "value": "x"
  },
  {
       "type": "Punctuator",
       "value": "+"
  },
  {
       "type": "Identifier",
       "value": "y"
  },
  {
       "type": "Punctuator",
       "value": ";"
  },
  {
       "type": "Punctuator",
       "value": "}"
  },
  {
       "type": "Keyword",
       "value": "else"
  },
  {
       "type": "Punctuator",
       "value": "{"
  },
  {
       "type": "Keyword",
       "value": "return"
  },
  {
       "type": "Identifier",
       "value": "x"
  },
  {
       "type": "Punctuator",
       "value": "+"
  },
  {
       "type": "String",
       "value": "'tadm'"
  },
  {
       "type": "Punctuator",
       "value": ";"
  },
  {
       "type": "Punctuator",
       "value": "}"
  },
  {
       "type": "Punctuator",
       "value": "}"
  }
]
  • AST

{
 "type": "Program",
 "body": [
  {
     "type": "FunctionDeclaration",
     "id": {
       "type": "Identifier",
       "name": "add"
    },
     "params": [
      {
         "type": "Identifier",
         "name": "x"
      },
      {
         "type": "Identifier",
         "name": "y"
      }
    ],
     "body": {
       "type": "BlockStatement",
       "body": [
        {
           "type": "IfStatement",
           "test": {
             "type": "BinaryExpression",
             "operator": "===",
             "left": {
               "type": "UnaryExpression",
               "operator": "typeof",
               "argument": {
                 "type": "Identifier",
                 "name": "x"
              },
               "prefix": true
            },
             "right": {
               "type": "Literal",
               "value": "number",
               "raw": "\"number\""
            }
          },
           "consequent": {
             "type": "BlockStatement",
             "body": [
              {
                 "type": "ReturnStatement",
                 "argument": {
                   "type": "BinaryExpression",
                   "operator": "+",
                   "left": {
                     "type": "Identifier",
                     "name": "x"
                  },
                   "right": {
                     "type": "Identifier",
                     "name": "y"
                  }
                }
              }
            ]
          },
           "alternate": {
             "type": "BlockStatement",
             "body": [
              {
                 "type": "ReturnStatement",
                 "argument": {
                   "type": "BinaryExpression",
                   "operator": "+",
                   "left": {
                     "type": "Identifier",
                     "name": "x"
                  },
                   "right": {
                     "type": "Literal",
                     "value": "tadm",
                     "raw": "'tadm'"
                  }
                }
              }
            ]
          }
        }
      ]
    },
     "generator": false,
     "expression": false,
     "async": false
  }
],
 "sourceType": "script"
}

解释

该阶段就是将上面产生的 AST 转换成字节码。

这里增加字节码(中间产物)的好处是,并不是将 AST 直接翻译成机器码,因为对应的 cpu 系统会不一致,翻译成机器码时要结合每种 cpu 底层的指令集,这样实现起来代码复杂度会非常高;还有个就是内存占用的问题,因为机器码会存储在内存中,而退出进程后又会存储在磁盘上,加上转换后的机器码多出来很多信息,会比源文件大很多,导致了严重的内存占用问题。

V8 在执行字节码的过程中,使用到了通用寄存器累加寄存器,函数参数和局部变量保存在通用寄存器里面,累加器中保存中间计算结果,在执行指令的过程中,如果直接由 cpu 从内存中读取数据的话,比较影响程序执行的性能,使用寄存器存储中间数据的设计,可以大大提升 cpu 执行的速度。

编译

这个过程主要是 V8 的 TurboFan编译器 将字节码翻译成机器码的过程。

字节码配合解释器和编译器这一技术设计,可以称为JIT(即时编译技术),Java 虚拟机也是类似的技术,解释器在解释执行字节码时,会收集代码信息,标记一些热点代码(就是一段代码被重复执行多次),TurboFan 会将热点代码直接编译成机器码,缓存起来,下次调用直接运行对应的二进制的机器码,加快执行速度。

在 TurboFan 将字节码编译成机器码的过程中,还进行了简化处理:常量合并、强制折减、代数重新组合。

比如:3 + 4 --> 7,x + 1 + 2 --> x + 3 ......

执行

到这里我们就开始执行上一阶段产出的机器码。

而在 JS 的执行过程中,经常遇到的就是对象属性的访问。作为一种动态的语言,一个简单的属性访问可能包含着复杂的语义,比如Object.xxx的形式,可能是属性的直接访问,也可能去调用的对象的Getter方法,还有可能是要通过原型链往上层对象中查找。这种不确定性而且动态判断的情况,会浪费很多查找时间,所以 V8 会把第一次分析的结果放在缓存中,当再次访问相同的属性时,会优先从缓存中去取,调用 GetProperty(Object, "xxx", feedback_cache) 的方法获取缓存,如果有缓存结果,就会跳过查找过程,又大大提升了运行性能。

除了上面针对读取对象属性的结果缓存的优化,V8 还引入了 Object Shapes(隐藏类)的概念,这里面会记录一些对象的基本信息(比如对象拥有的所有属性、每个属性对于这个对象的偏移量等),这样我们去访问属性时就可以直接通过属性名和偏移量直接定位到他的内存地址,读取即可,大大提升访问效率。

既然 V8 提出了隐藏类(两个形状相同的对象会去复用同一个隐藏类,何为形状相同的对象?两个对象满足有相同个数的相同属性名称和相同的属性顺序),那么我们开发者也可以很好的去利用它:

  • 尽量创建形状相同的对象

  • 创建完对象后尽量不要再去操作属性,即不增加或者删除属性,也就不会破环对象的形状

完成

到此 V8 已经完成了一份 JS 代码的读取、分析、解释、编译、执行。

总结

以上就是从 JS 代码下载到最终在 V8 引擎执行的过程分析,可以发现 V8 其实有很多实现的技术点,有着很巧妙的设计思想,比如流式处理、缓存中间产物、垃圾回收等,这里面又会涉及到很多细节,很值得继续深入研究。

作者:Tadm
来源:https://juejin.cn/post/7032278688192430117

收起阅读 »

手写清除console的loader

前言删除console方式介绍通过编辑器查找所有console,或者eslint编译时的报错提示定位语句,然后清除,就是有点费手,不够优雅 因此下面需要介绍几种优雅的清除方式该插件可用于压缩我们的js代码,同时可以通过配置去掉console语句,安装后配置在...
继续阅读 »




前言

作为一个前端,对于console.log的调试可谓是相当熟悉,话不多说就是好用!帮助我们解决了很多bug^_^
但是!有因必有果(虽然不知道为什么说这句但是很顺口),如果把console发到生产环境也是很头疼的,尤其是如果打印的信息很私密的话,可能要凉凉TT

删除console方式介绍

对于在生产环境必须要清除的console语句,如果手动一个个删除,听上去就很辛苦,因此这篇文章本着看到了就要学,学到了就要用的精神我打算介绍一下手写loader的方式清除代码中的console语句,在此之前也介绍一下其他可以清除console语句的方式吧哈哈

1. 方式一:暴力清除

通过编辑器查找所有console,或者eslint编译时的报错提示定位语句,然后清除,就是有点费手,不够优雅
因此下面需要介绍几种优雅的清除方式

2. 方式二 :uglifyjs-webpack-plugin

该插件可用于压缩我们的js代码,同时可以通过配置去掉console语句,安装后配置在webpack的optimization下,即可使用,需要注意的是:此配置只在production环境下生效

安装
npm i uglifyjs-webpack-plugin

其中drop_console和pure_funcs的区别是:

  • drop_console的配置值为boolean,也就是说如果为true,那么代码中所有带console前缀的调试方式都会被清除,包括console.log,console.warn等

  • pure_funcs的配置值是一个数组,也就是可以配置清除那些带console前缀的语句,截图中配的是['console.log'],因此生产环境上只会清除console.log,如果代码中包含其他带console的前缀,如console.warn则保留

但是需要注意的是,该方法只对ES5语法有效,如果你的代码中涉及ES6就会报错

3. 方式三:terser-webpack-plugin

webpack v5 开箱即带有最新版本的 terser-webpack-plugin。如果你使用的是 webpack v5 或更高版本,同时希望自定义配置,那么仍需要安装 terser-webpack-plugin。如果使用 webpack v4,则必须安装 terser-webpack-plugin v4 的版本。

安装
npm i terser-webpack-plugin@4

terser-webpack-plugin对于清楚console的配置可谓是跟uglifyjs-webpack-plugin一点没差,但是他们最大的差别就是TerserWebpackPlugin支持ES6的语法

4. 方式四:手写loader删除console

终于进入了主题了,朋友们

  1. 什么是loader

众所周知,webpack只能理解js,json等文件,那么除了js,json之外的文件就需要通过loader去顺利加载,因此loader在其中担任的就是翻译工作。loader可以看作一个node模块,实际上就是一个函数,但他不能是一个箭头函数,因为它需要继承webpack的this,可以在loader中使用webpack的方法。

  • 单一原则,一个loader只做一件事

  • 调用方式,loader是从右向左调用,遵循链式调用

  • 统一原则,输入输出都是字符串或者二进制数据

根据第三点,下面的代码就会报错,因为输出的是数字而不是字符串或二进制数据

module.exports = function(source) {
  return 111
}

  1. 新建清除console语句的loader

首先新建一个dropConsole.js文件

// source:表示当前要处理的内容
const reg = /(console.log\()(.*)(\))/g;
module.exports = function(source) {
  // 通过正则表达式将当前处理内容中的console替换为空字符串
  source = source.replace(reg, "")
  // 再把处理好的内容return出去,坚守输入输出都是字符串的原则,并可达到链式调用的目的供下一个loader处理
  return source
}
  1. 在webpack的配置文件中引入

module: {
  rules:[
      {
          test: /\.js/,
          use: [
              {
              loader: path.resolve(__dirname, "./dropConsole.js"),
              options: {
                name: "前端"
              }
              }
          ]
      },
    {
  ]
}

在webpack的配置中,loader的导入需要绝对路径,否则导入失效,如果想要像第三方loader一样引入,就需要配置resolveLoader 中的modules属性,告诉webpack,当node_modules中找不到时,去别的目录下找

module: {
  rules:[
      {
          test: /\.js/,
          use: [
              {
              loader: 'dropConsole',
              options: {
                name: "前端"
              }
              }
          ]
      },
    {
  ]
}
resolveLoader:{
  modules:["./node_modules","./build"] //此时我的loader写在build目录下
},

正常运行后,调试台将不会打印console信息

  1. 最后介绍几种在loader中常用的webpack api

  • this.query:返回webpack的参数即options的对象

  • this.callback:同步模式,可以把自定义处理好的数据传递给webpack

const reg = /(console.log\()(.*)(\))/g;
module.exports = function(source) {
  source = source.replace(reg, "");
  this.callback(null,source);
  // return的作用是让webpack知道loader返回的结果应该在this.callback当中,而不是return中
  return    
}
  • this.async():异步模式,可以大致的认为是this.callback的异步版本,因为最终返回的也是this.callback

const  path = require('path')
const util = require('util')
const babel = require('@babel/core')


const transform = util.promisify(babel.transform)

module.exports = function(source,map,meta) {
var callback = this.async();

transform(source).then(({code,map})=> {
    callback(null, code,map)
}).catch(err=> {
    callback(err)
})
};

最后的最后,webpack博大精深,值得我们好好学习,深入研究!

作者:我也想一夜暴富
来源:https://juejin.cn/post/7038413043084034062

收起阅读 »

uniapp热更新

热更新主要是针对app上线之后页面出现bug,修改之后又得打包,上线,每次用户都得在应用市场去下载很影响用户体验,如果用户不愿意更新,一直提示都不愿意更新,这个bug就会一直存在。 可能你一不小心写错了代码,整个团队的努力都会付之东流,苦不苦,冤不冤,想想都苦...
继续阅读 »



为什么要热更新

热更新主要是针对app上线之后页面出现bug,修改之后又得打包,上线,每次用户都得在应用市场去下载很影响用户体验,如果用户不愿意更新,一直提示都不愿意更新,这个bug就会一直存在。 可能你一不小心写错了代码,整个团队的努力都会付之东流,苦不苦,冤不冤,想想都苦,所以这个时候热更新就显得很重要了。

首先你需要在manifest.json 中修改版本号

如果之前是1.0.0那么修改之后比如是1.0.1或者1.1.0这样

然后你需要在HBuilderX中打一个wgt包

在顶部>发行>原生App-制作移动App资源升级包

包的位置会在控制台里面输出

你需要和后端约定一下接口,传递参数

然后你就可以在app.vue的onLaunch里面编写热更新的代码了,如果你有其他需求,你可以在其他页面的onLoad里面编写。

// #ifdef APP-PLUS  //APP上面才会执行
plus.runtime.getProperty(plus.runtime.appid,
function(widgetInfo) {
uni.request({
url: '请求url写你自己的',
method: "POST",
data: {
version: widgetInfo.version,
//app版本号
name: widgetInfo.name //app名称
},
success: (result) = >{
console.log(result) //请求成功的数据
var data = result.data.data
if (data.update && data.wgtUrl) {
var uploadTask = uni.downloadFile({ //下载
url: data.wgtUrl,
//后端传的wgt文件
success: (downloadResult) = >{ //下载成功执行
if (downloadResult.statusCode === 200) {
plus.runtime.install(downloadResult.tempFilePath, {
force: flase
},
function() {
plus.runtime.restart();
},
function(e) {});
}
},
}) uploadTask.onProgressUpdate((res) = >{
// 测试条件,取消上传任务。
if (res.progress == 100) { //res.progress 上传进度
uploadTask.abort();
}
});
}
}
});
});
// #endif

不支持的情况

  • SDK 部分有调整,比如新增了 Maps 模块等,不可通过此方式升级,必须通过整包的方式升级。

  • 原生插件的增改,同样不能使用此方式。
    对于老的非自定义组件编译模式,这种模式已经被淘汰下线。但以防万一也需要说明下,老的非自定义组件编译模式,如果之前工程没有 nvue 文件,但更新中新增了 nvue 文件,不能使用此方式。因为非自定义组件编译模式如果没有nvue文件是不会打包weex引擎进去的,原生引擎无法动态添加。自定义组件模式默认就含着weex引擎,不管工程下有没有nvue文件。

注意事项

  • 条件编译,仅在 App 平台执行此升级逻辑。

  • appid 以及版本信息等,在 HBuilderX 真机运行开发期间,均为 HBuilder 这个应用的信息,因此需要打包自定义基座或正式包测试升级功能。

  • plus.runtime.version 或者 uni.getSystemInfo() 读取到的是 apk/ipa 包的版本号,而非 manifest.json 资源中的版本信息,所以这里用 plus.runtime.getProperty() 来获取相关信息。

  • 安装 wgt 资源包成功后,必须执行 plus.runtime.restart(),否则新的内容并不会生效。

  • 如果App的原生引擎不升级,只升级wgt包时需要注意测试wgt资源和原生基座的兼容性。平台默认会对不匹配的版本进行提醒,如果自测没问题,可以在manifest中配置忽略提示,详见ask.dcloud.net.cn/article/356…

  • http://www.example.com 是一个仅用做示例说明的地址,实际应用中应该是真实的 IP 或有效域名,请勿直接复制粘贴使用。

关于热更新是否影响应用上架

应用市场为了防止开发者不经市场审核许可,给用户提供违法内容,对热更新大多持排斥态度。

但实际上热更新使用非常普遍,不管是原生开发中还是跨平台开发。

Apple曾经禁止过jspatch,但没有打击其他的热更新方案,包括cordovar、react native、DCloud。封杀jspatch其实是因为jspatch有严重安全漏洞,可以被黑客利用,造成三方黑客可篡改其他App的数据。

使用热更新需要注意:

  • 上架审核期间不要弹出热更新提示

  • 热更新内容使用https下载,避免被三方网络劫持

  • 不要更新违法内容、不要通过热更新破坏应用市场的利益,比如iOS的虚拟支付要老老实实给Apple分钱

如果你的应用没有犯这些错误,应用市场是不会管的。

作者:是一个秃头
来源:https://juejin.cn/post/7039273141901721608

收起阅读 »

GC回收机制与分代回收策略

GC回收机制一、前言垃圾回收:Garbage Collection,简写 GC。JVM 中的垃圾回收器会自动回收无用的对象。但是 GC 自动回收的代价是:当这种自动化机制出错,我们就需要深入理解 GC 回收机制,甚至需要对这些 自动化 的技术实施必要的监控与...
继续阅读 »



GC回收机制

一、前言

垃圾回收Garbage Collection,简写 GCJVM 中的垃圾回收器会自动回收无用的对象。

但是 GC 自动回收的代价是:当这种自动化机制出错,我们就需要深入理解 GC 回收机制,甚至需要对这些 自动化 的技术实施必要的监控与调节。

在虚拟机中,程序计数器、虚拟机栈、本地方法栈 3 个区域随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而执行着出栈和入栈操作。所以这几个区域不需要考虑回收的问题。

而在 堆和方法区 中,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样。这部分的只有在程序运行期间才会知道需要创建哪些对象,这部分的内存的创建和回收是动态的,也是垃圾回收器重点关注的地方。

二、什么是垃圾

垃圾 就是 内存中已经没有用的对象。既然是 垃圾回收,就必须知道哪些对象是垃圾。

Java 虚拟机中使用了一种叫做 可达性分析 的算法 来决定对象是否可以被回收

GCRoot示意图

上图中 A、B、C、D、E 与 GCRoot 直接或间接产生引用链,所以 GC 扫描到这些对象时,并不会执行回收操作;J、K、M虽然之间有引用链,但是并没有与 GCRoot 存在引用链,所以当 GC 扫描到他们时会将他们回收。

注意的是,上图中所有的对象,包括 GCRoot,都是内存中的引用。

作为 GCRoot 的几种对象
  1. Java虚拟机栈(局部变量表)中的引用的对象;

  2. 方法区中静态引用指向的对象;

  3. 仍处于存活状态中的线程对象;

  4. Native方法中 JNI 引用的对象;

三、什么时候回收

不同的虚拟机实现有着不同的 GC 实现机制,但一般情况下都会存在下面两种情况:

  1. Allocation Failure:在堆内存分配中,如果因为可用剩余空间不足导致对象内存分配失败,这时系统会触发一次 GC。

  2. System.gc():在应用层,Java开发工程师可以主动调用此API来请求一次 GC。

四、验证GCRoot的几种情况

在验证之前,先了解Java命令时的参数。

-Xms:初始分配 JVM 运行时的内存大小,如果不指定则默认为物理内存的 1/64

举个小例子

// 表示从物理内存中分配出 200M 空间给 JVM 内存
java -Xms200m HelloWorld
1.验证虚拟机栈(栈帧中的局部变量)中引用的对象作为 GCRoot
// 验证代码
public class GCRootLocalVariable {

  private int _10MB = 10 * 1024 * 1024;
  private byte[] memory = new byte[8 * _10MB];

  public static void main(String[] args) {
      System.out.println("开始时:");
      printMemory();
      method();
      System.gc();
      System.out.println("第二次GC完成");
      printMemory();
  }

  public static void method() {
      GCRootLocalVariable gc = new GCRootLocalVariable();
      System.gc();
      System.out.println("第一次GC完成");
      printMemory();
  }

  // 打印出当前JVM剩余空间和总的空间大小
  public static void printMemory() {
      long freeMemory = Runtime.getRuntime().freeMemory();
      long totalMemory = Runtime.getRuntime().totalMemory();
      System.out.println("剩余空间:" + freeMemory / 1024 / 1024 + "M");
      System.out.println("总共空间:" + totalMemory / 1024 / 1024 + "M");
  }
}
// 打印日志:
开始时:
剩余空间:119M
总共空间:123M
第一次GC完成
剩余空间:40M
总共空间:123M
第二次GC完成
剩余空间:120M
总共空间:123M

从上述代码中可以看到:

第一次打印内存信息,分别为 119M 和 123M;

第二次打印内存信息,分别为 40M 和 123M;剩余空间小了 80M,是因为在 method() 方法中创建了局部变量 gc(位于栈帧中的局部变量),并且这个 gc 对象会被作为 GCRoot。虽然创建的对象未被使用并且调用了 System.gc(),但是因为该方法未结束,所以创建的对象不能被回收。

第三次打印内存信息,分别为 120M 和 123M;method() 方法已经结束,创建的对象 gc 也随方法消失,不再有引用类型指向该 80M 对象。

【值得注意的是】

private int _10MB = 10 * 1024 * 1024;
private byte[] memory = new byte[8 * _10MB];

上面 2 行代码是必须的,如果去掉,那么 3 次打印结果将会一致,idea 也会出现Instantiation of utility class 警告信息,说这个类只存在静态方法,没必要创建这个对象。

这也就是说为什么创建 GCRootLocalVariable() 会需要 80M 的大小,是因为 GCRootLocalVariable 在创建时就会为其内部变量 memory 确定 80M 的大小。

2.验证方法区中的静态变量引用的对象作为 GCRoot
public class GCRootStaticVariable {
  private static int _10M = 10 * 1024 * 1024;
  private byte[] memory;
  private static GCRootStaticVariable staticVariable;

  public GCRootStaticVariable(int size) {
      memory = new byte[size];
  }

  public static void main(String[] args) {
      System.out.println("程序开始:");
      printMemory();
      GCRootStaticVariable g = new GCRootStaticVariable(2 * _10M);
      g.staticVariable = new GCRootStaticVariable(4 * _10M);
      // 将g设置为null,调用GC时可以回收此对象内存
      g = null;
      System.gc();
      System.out.println("GC完成");
      printMemory();
  }

  // 打印JVM剩余空间和总空间
  private static void printMemory() {
      long freeMemory = Runtime.getRuntime().freeMemory();
      long totalMemory = Runtime.getRuntime().totalMemory();
      System.out.println("剩余空间" + freeMemory/1024/1024 + "M");
      System.out.println("总共空间" + totalMemory/1024/1024 + "M");
  }
}

打印结果:
程序开始:
剩余空间119M
总共空间123M
GC完成
剩余空间81M
总共空间123M

通过上述打印结果可知:

  1. 程序刚开始时打印结果为 119M;

  2. 当创建 g 对象时分配 20M 内存,又为静态变量 staticVariable 分配 40M 内存;

  3. 当调用 gc 回收时,非静态变量 memory 分配的 20M 内存被回收;

  4. 但是作为 GCRoot 的静态变量 staticVariable 不会被回收,所以最终打印结果少了 40M 内存。

3.验证活跃线程作为GCRoot
public class GCRootThread {

  private int _10M = 10 * 1024 * 1024;
  private byte[] memory = new byte[8 * _10M];

  public static void main(String[] args) throws InterruptedException {
      System.out.println("程序开始:");
      printMemory();
      AsyncTask asyncTask = new AsyncTask(new GCRootThread());
      Thread thread = new Thread(asyncTask);
      thread.start();
      System.gc();
      System.out.println("main方法执行完成,执行gc");
      printMemory();
      thread.join();
      asyncTask = null;
      System.gc();
      System.out.println("线程代码执行完成,执行gc");
      printMemory();
  }

  private static void printMemory() {
      long freeMemory = Runtime.getRuntime().freeMemory();
      long totalMemory = Runtime.getRuntime().totalMemory();
      System.out.println("剩余内存:" + freeMemory / 1024 / 1024 + "M");
      System.out.println("总共内存:" + totalMemory / 1024 / 1024 + "M");
  }

  private static class AsyncTask implements Runnable {

      private GCRootThread gcRootThread;

      public AsyncTask(GCRootThread gcRootThread) {
          this.gcRootThread = gcRootThread;
      }

      @Override
      public void run() {
          try {
              Thread.sleep(500);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
      }
  }
}
打印结果:
程序开始:
剩余内存:119M
总共内存:123M
main方法执行完成,执行gc
剩余内存:41M
总共内存:123M
线程代码执行完成,执行gc
剩余内存:120M
总共内存:123M

通过上述打印结果可知:

  1. 程序刚开始时可用内存为 119M;

  2. 第一次调用 gc 时,线程并没有执行结束,并且它作为 GCRoot ,所以它所引用的 80M 内存不会被 GC 回收掉;

  3. thread.join() 保证线程结束后再调用后续代码,所以当第二次调用 GC 时,线程已经执行完毕并被置为 null;

  4. 这时线程已经销毁,所以该线程所引用的 80M 内存被 GC 回收掉。

4.测试成员变量是否可作为GCRoot
public class GCRootClassVariable {
  private static int _10M = 10 * 1024 * 1024;
  private byte[] memory;
  private GCRootClassVariable gcRootClassVariable;

  public GCRootClassVariable(int size) {
      memory = new byte[size];
  }

  public static void main(String[] args) {
      System.out.println("程序开始:");
      printMemory();
      GCRootClassVariable g = new GCRootClassVariable(2 * _10M);
      g.gcRootClassVariable = new GCRootClassVariable(4 * _10M);
      g = null;
      System.gc();
      System.out.println("GC完成");
      printMemory();
  }

  private static void printMemory() {
      long freeMemory = Runtime.getRuntime().freeMemory();
      long totalMemory = Runtime.getRuntime().totalMemory();
      System.out.println("剩余内存:" + freeMemory / 1024 / 1024 + "M");
      System.out.println("总共内存:" + totalMemory / 1024 / 1024 + "M");
  }
}
打印结果:
程序开始:
剩余内存:119M
总共内存:123M
GC完成
剩余内存:121M
总共内存:123M

上述打印结果可知:

  1. 第一次打印结果与第二次打印结果一致:全局变量 gcRootClassVariable 随着 g=null 后被销毁。

  2. 所以全局变量并不能作为 GCRoot。

五、如何回收垃圾(常见的几种垃圾回收算法)

1.标记清除算法(Mark and Sweep GC)

从 “GCRoots” 集合开始,将内存整个遍历一次,保留所有可以被 GCRoots 直接或间接引用到的对象,而剩下的对象都当做垃圾对待并回收。

上述整个过程分为两步:

  1. Mark标记阶段:找到内存中的所有 GC Root 对象,只要是和 GC Root 对象直接或间接相连则标记为灰色(存活对象),否则标记为黑色(垃圾对象)。

  2. Sweep清楚阶段:当遍历完所有的 GC Root 之后,则将标记为垃圾的对象直接清楚。

标记清除算法示意图

标记清除算法优缺点

【优点】

实现简单,不需要将对象进行移动。

【缺点】

需要中断进程内其他组件的执行,并且可能产生内存碎片,提高了垃圾回收的频率。

2.复制算法(Copying)
  1. 将现有的内存空间分为两块,每次只使用其中一块;

  2. 在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中;

  3. 之后清除正在使用的内存块中的所有对象;

  4. 交换两个内存的角色,完成垃圾回收(目前使用A,B是空闲,算法完成后A为空闲,设置B为使用状态)。

复制算法复制前示意图

复制算法复制后示意图

复制算法优缺点

【优点】

按顺序分配内存即可;实现简单、运行高效,不用考虑内存碎片问题。

【缺点】

可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。

3.标记压缩算法(Mark-Compact)
  1. 需要先从根节点开始对所有可达对象做一次标记;

  2. 之后并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端;

  3. 最后清理边界外所有的空间。

所有,标记压缩也分为两步完成:

  1. Mark标记阶段:找到内存中的所有 GC Root 对象,只要和 GC Root 对象直接或间接相连则标记为灰色(存活对象),否则标记为黑色(垃圾对象)

  2. Compact压缩阶段:将剩余存活对象按顺序压缩到内存的某一端

标记压缩算法示意图

标记压缩算法优缺点

【优点】

避免了碎片产生,又不需要两块相同的内存空间,性价比较高。

【缺点】

所谓压缩操作,仍需要进行局部对象移动,一定程度上还是降低了效率。

分代回收策略

Java 虚拟机根据对象存活的周期不同,把堆内存划分为 新生代老年代,这就是 JVM 的内存分代策略。

注意:在 HotSpot 中除了 新生代老年代,还有 永久代

分代回收的中心思想:对于新创建的对象会在新生代中分配内存,此区域的对象生命周期一般较短,如果经过多次回收仍然存活下来,则将它们转移到老年代中。

一、年轻代

新生成的对象优先存放在新生代中,存活率很低

新生代中,常规应用进行一次垃圾收集一般可以回收 70% ~ 95% 的空间,回收效率很高。所以一般采用的 GC 回收算法是 复制算法

新生代也可细分3部分:Eden、Survivor0(简称 S0)、Survivor1(简称 S1),这 3 部分按照 8:1:1 的比例来划分新生代。

新生代老年代示意图

新生成的对象会存放在 Eden 区。

新生代老年代示意图

当 Eden 区满时,会触发垃圾回收,回收掉垃圾之后,将剩下存活的对象存放到 S0 区。当下一次 Eden 区满时,再次触发垃圾回收,这时会将 Eden 区 和 S0 区存活的对象全部复制到 S1 区,并清空 Eden 区和 S0 区。

新生代老年代示意图

上述步骤重复 15 次之后,依然存活下来的对象存放到 老年区

二、老年代

一个对象如果在新生代存活了足够长的时间而没有被清理掉,则会被复制到老年代。

老年代的内存大小一般比新生代大,能存放更多的对象。如果对象比较大(比如长字符串或大数组),并且新生代的剩余空间不足,则这个大对象会直接被分配到老年代上。

可以使用 -XX:PretenureSizeThreshold 来控制直接升入老年代的对象大小。

因为老年代对象的生命周期较长,不需要过多的复制操作,所以一般采用标记压缩的回收算法。

【注意的是】

有这么一种情况,老年代中的对象会引用新生代中的对象,这时如果要执行新生代的 GC,则可能要查询整个老年代引用新生代的情况,这种效率是极低的。所以老年代中维护了一个 512byte 的 table,所有老年代对象引用新生代对象的引用都记录在这里。这样新生代 GC 时只需要查询这个表即可。

三、GC log分析

为了让上层应用开发人员更加方便调试 Java 程序,JVM 提供了相应的 GC 日志,在 GC 执行垃圾回收事件中,会有各种相应的 log 被打印出来。

新生代和老年代打印的日志是有区别的:

【新生代GC:轻GC】这一区域的 GC 叫做 Minor GC。因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度比较快。

【老年代GC:重GC】发生在这一区域的 GC 叫做 Major GC 或者 Full GC,当出现 Major GC,经常会伴随至少一次 Minor GC

Major GCFull GC 在有些虚拟机中还是有区别的:前者是仅回收老年代中的垃圾对象,后者是回收整个堆中的垃圾对象。

常用的 GC 命令参数
命令参数功能描述
-verbose:gc显示 GC 的操作内容
-Xms20M初始化堆大小为 20M
-Xmx20M设置堆最大分配内存 20M
-Xmn10M设置新生代的内存大小为 10M
-XX:+PrintGCDetails打印GC的详细log日志
-XX:SurvivorRatio=8新生代中 Eden 区域与 Survivor 区域的大小比值为 8:1:1

添加 VM Options 参数:分配堆内存 20M,10M给新生代,10M给老年代

// VM args: -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
public class MinorGCTest {

  private static final int _1M = 1024 * 1024;

  public static void main(String[] args) {
      byte[] a, b, c, d;
      a = new byte[2 * _1M];
      b = new byte[2 * _1M];
      c = new byte[2 * _1M];
      d = new byte[_1M];
  }
}
打印结果:(这里测试是第二次修改后的运行效果)
[GC (Allocation Failure) [PSYoungGen: 7820K->840K(9216K)] 7820K->6992K(19456K), 0.0072302 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[Full GC (Ergonomics) [PSYoungGen: 840K->0K(9216K)] [ParOldGen: 6152K->6759K(10240K)] 6992K->6759K(19456K), [Metaspace: 3198K->3198K(1056768K)], 0.0087734 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
PSYoungGen     total 9216K, used 1190K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 14% used [0x00000000ff600000,0x00000000ff7298d8,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen       total 10240K, used 6759K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 66% used [0x00000000fec00000,0x00000000ff299cd0,0x00000000ff600000)
Metaspace       used 3205K, capacity 4496K, committed 4864K, reserved 1056768K
class space   used 351K, capacity 388K, committed 512K, reserved 1048576K

上述字段意思代表如下:

字段代表含义
PSYoungGen新生代
eden新生代中的 Eden 区
from新生代中的 S0 区
to新生代中的 S1 区
ParOldGen老年代
  1. 第一次运行效果后,因为 Eden 区 8M,S0 和 S1 各 1M。所以 a、b、c、d 共有 7M 空间都会在 Eden 区。

  2. 修改 d = new byte[2 * _1M],再次运行;

  3. JVM 会将 a/b/c 存放到 Eden 区,Eden 占有 6M 空间,无法再分配 2M 空间给 d;

  4. 因此会执行一次轻 GC,并尝试将 a/b/c 复制到 S1 区;

  5. 但是因为 S1 区只有 1M 空间,所以没办法存储 a/b/c 三者任一对象。

  6. 这种情况下,JVM 将 a/b/c 转移到老年代,将 d 保存在 Eden 区。

【最终结果】

Eden区 占用 2M 空间(d),老年代占用 6M 空间(a,b,c)

四、引用

通过 GC Roots 的引用可达性来判断对象是否存活,JVM 中的引入关系有以下四种:

引用英文名GC回收机制使用示例
强引用Strong Reference如果一个对象具有强引用,那么垃圾回收期绝不会回收它Object obj = new Object();
软引用Soft Reference在内存实在不足时,会对软引用进行回收SoftReference softObj = new SoftReference();
弱引用Weak Reference第一次GC回收时,如果垃圾回收器遍历到此弱引用,则将其回收WeakReference weakObj = new WeakReference();
虚引用Phantom Reference一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象的实例不会使用
软引用的用法
public class SoftReferenceNormal {

  static class SoftObject {
      byte[] data = new byte[120 * 1024 * 1024]; // 120M
  }

  public static void main(String[] args) {
      SoftReference<SoftObject> softObj = new SoftReference<>(new SoftObject());
      System.out.println("第一次GC前,软引用:" + softObj.get());
      System.gc();
      System.out.println("第一次GC后,软引用:" + softObj.get());
      SoftObject obj = new SoftObject();
      System.out.println("分配100M强引用,软引用:" + softObj.get());
  }
}

添加 VM Option 参数:-Xmx200M 给堆内存分配最大200M内存

第一次 GC 前,软引用:SoftReferenceNormal$SoftObject@1b6d3586
第一次 GC 后,软引用:SoftReferenceNormal$SoftObject@1b6d3586
分配 100M 强引用,软引用:null
  1. 添加参数后位堆内存分配最大 200M 空间,分配给 softObj 对象 120M。

  2. 第一次 GC 后,因为剩余内存任然够,所以软引用并没有被回收。

  3. 当分配 100M 强引用后,堆内存空间不够,会触发GC回收,回收掉软引用。

软引用隐藏的问题

【注意】

被软引用对象关联的对象会自动被垃圾回收器回收,但是软引用对象本身也是一个对象,这些创建的软引用并不会自动被垃圾回收器回收掉。

public class SoftReferenceTest {

  static class SoftObject {
      byte[] data = new byte[1024]; // 占用1k空间
  }

  private static final int _100K = 100 * 1024;
  // 静态集合保存软引用,会导致这些软引用对象本身无法被垃圾回收器回收
  private static Set<SoftReference<SoftObject>> cache = new HashSet<>(_100K);

  public static void main(String[] args) {
      for (int i = 0; i < _100K; i++) {
          SoftObject obj = new SoftObject();
          cache.add(new SoftReference(obj));
          if (i * 10000 == 0) {
              System.out.println("cache size is " + cache.size());
          }
      }
      System.out.println("END");
  }
}

添加 VM Option 参数:-Xms4m -Xmx4m -Xmn2m

// 打印结果:
cache size is 1
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at SoftReferenceTest$SoftObject.<init>(SoftReferenceTest.java:8)
at SoftReferenceTest.main(SoftReferenceTest.java:17)

程序崩溃,崩溃的原因并不是堆内存溢出,而是超出了 GC 开销限制。

这里错误的原因是:JVM 不停的回收软引用中的对象,回收次数过快,回收内存较小,占用资源过高了。

【解决方案】注册一个引用队列,将这个对象从 Set 中移除掉。

public class SoftReferenceTest {

  static class SoftObject {
      byte[] data = new byte[1024]; // 占用1k空间
  }

  private static final int _100K = 100 * 1024;
  // 静态集合保存软引用,会导致这些软引用对象本身无法被垃圾回收器回收
  private static Set<SoftReference<SoftObject>> cache = new HashSet<>(_100K);
  // 解决方案:注册一个引用队列,将要移除的对象从中删除
  private static ReferenceQueue<SoftObject> queue = new ReferenceQueue<>();
  // 记录清空次数
  private static int removeReferenceIndex = 0;

  public static void main(String[] args) {
      for (int i = 0; i < _100K; i++) {
          SoftObject obj = new SoftObject();
          cache.add(new SoftReference(obj, queue));
          // 清除掉软引用
          removeSoft();
          if (i * 10000 == 0) {
              System.out.println("cache size is " + cache.size());
          }
      }
      System.out.println("END removeReferenceIndex: " + removeReferenceIndex);
  }

  private static void removeSoft() {
      Reference<? extends SoftObject> poll = queue.poll();
      while (poll != null) {
          if (cache.remove(poll)) {
              removeReferenceIndex++;
          }
          poll = queue.poll();
      }
  }
}
// 打印结果:
cache size is 1
END removeReferenceIndex: 101745

作者:沅兮
来源:https://juejin.cn/post/7037330678731505672


收起阅读 »

swift 键盘收起

iOS
直接调用就能收起键盘,无需调用其他方法        UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), t...
继续阅读 »







直接调用就能收起键盘,无需调用其他方法    

    UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)

收起阅读 »

iOS 底层原理探索 之 结构体内存对齐

iOS
写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之 路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。 目录如下:iOS 底层原理探索之 alloc以上内容的总结专栏iOS 底层原理探索 之 阶段总结准备Objective-C...
继续阅读 »


写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。

目录如下:

  1. iOS 底层原理探索之 alloc

以上内容的总结专栏


准备

Objective-C ,通常写作ObjC或OC,是扩充C的面向对象编程语言。它主要适用于Mac OS X 和 GNUstep者两个使用OpenStep标准的系统,而在NeXTSTEP和OpenStep中它更是基本语言。

GCC和Clang含Objective-C的编译器,Objective-C可以在GCC以及Clang运作的系统上编译。

我们平时开发用的Objective-C语言,编译后最终会转化成C/C++语言。

为什么要研究结构体的内存对齐呢? 因为作为一名iOS开发人员,随着对于底层的不断深入探究,我们都知道,所有的对象在底层中都是一个结构体。那么结构体的内存空间又会被系统分配多少空间,这个问题,值得我们一探究竟。

首先,从大神Cooci那里盗取了一张各数据类型占用的空间大小图片,作为今天探究结构体内存对齐原理的依据。

image.png

当我们创建一个对象的时候,我们并不需要过多的在意属性的顺序,因为系统会帮我们做优化处理。但是,在创建结构体的时候,就需要我们去分析了,因为这个时候系统并不会帮助我们做优化。

接下来,我们看下面两个结构体:

struct Struct1 {    
double a;
char b;
int c;
short d;
char e;
}struct1;

struct Struct2 {
double a;
int b;
char c;
short d;
char e;
}struct2;


两个结构体拥有的数据类型是相同的,按照图片中double 是8字节, char 是1字节, int 是4字节,short 是2字节, 那么 两个结构体应该是占 16字节的内存空间,也就是分配16字节空间即可,然而,我们看下面的结果:

    printf("%lu--%lu", sizeof(struct1), sizeof(struct2));
------------
24--16

那么,这就是有问题的了,两个拥有相同数据类型的结构体,被系统分配到的内存空间是不一样的,这是为什么呢?今天的重点就是这里,结构体的

内存对齐原则:

1:数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第
一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置
要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是
数组,结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址
开始存储。

2:结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从
其内部最大元素大小的整数倍地址开始存储。(struct a里存有struct b,
b里有char,int ,double等元素,那b应该从8的整数倍开始存储)

3:收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员
的整数倍,不足的要补⻬。

那么,我们按照以上内存对齐原则再来分析下 struct1 和 struct2 :


struct Struct1 { /// 18 --> 24
double a; //8 [0 1 2 3 4 5 6 7]
char b; //1 [8 ]
int c; //4 [9 [12 13 14 15]
short d; //2 [16 17]
char e; //1 [18]
}struct1;


struct Struct2 { /// 16 --> 16
double a; //8 [0 1 2 3 4 5 6 7]
int b; //4 [8 9 10 11]
char c; //1 [ 12 ]
short d; //2 [14 15]
char e; // 1 [16]
}struct2;


接着,我们看下下面的结构体

struct Struct3 {    
double a;
int b;
char c;
short d;
int e;
struct Struct1 str;
}struct3;


打印输出结果为 48 ,分析如下:

    double a;           //8 [0 1 2 3 4 5 6 7]
int b; //4 [8 9 10 11]
char c; //1 [12]
short d; //2 [ 14 15 ]
int e; //4 [ 16 17 18 19]
struct Struct1 str; //24 [24 ... 47]

所以,struct3 大小为48。


猜想:内存对齐的收尾工作中的内部最大成员指的是什么的大小呢?

接下来我们来一一验证一下

struct LGStruct4 {          /// 40 --> 48 
double a; //8 [0 1 2 3 4 5 6 7]
int b; //4 [8 9 10 11]
char c; //1 [12]
short d; //2 [14 15]
int e; //4 [16 17 18 19]
struct Struct2 str; //16 [24 ... 39]
}struct4;

按照我对于内存对齐原则中收尾工作的理解, 最终的大小 应该是 Struct2 的 大小 16 的整数倍 也就是 48 才对。然而, 结果却是:

    NSLog(@"%lu", sizeof(struct4));
--------
SMObjcBuild[8076:213800] 40

对,是40你没有看错,这样的话,很显然,我理解的就是错误的, 结构体内部最大成员应该指的是这里的 double,那么我们接下来验证一下: 1、

struct Struct2 {    ///16
double a; //8 [0 1 2 3 4 5 6 7]
int b; //4 [8 9 10 11]
char c; //1 [ 12 ]
short d; //2 [14 15]
}struct2;

struct LGStruct4 { /// 24

short d; //2 [0 1]

struct Struct2 str; // 16 [8 ... 23]

}struct4;

结果是 :24


因为,结构体内部最大成员是 double也就是8;并不是按照 LGStruct4中的str长度为16的整数倍来计算,所以最后的结果是24。

总结

结构体内部最大成员指的是结构体内部的数据类型,所以,结构体内包含结构体的时候,并不是按照内部的结构体长度的整数倍来计算的哦。


收起阅读 »

iOS 底层原理探索 之 alloc

iOS
iOS 底层原理探索 之 alloc写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之 路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。 内容的总结专栏iOS 底层原理探索 之 阶段总结序作为一名iOS开发人员,在平时开发工...
继续阅读 »

iOS 底层原理探索 之 alloc

写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。


内容的总结专栏


作为一名iOS开发人员,在平时开发工作中,所有的对象我们使用最多的是alloc来创建。那么alloc底层做了哪些操作呢?接下来我会一步一步探究alloc方法的底层实现。

初探

我们先来看下面的代码

    SMPerson *p1 = [SMPerson alloc];
SMPerson *p2 = [p1 init];
SMPerson *p3 = [p1 init];

NSLog(@"%@-%p-%p", p1, p1, &p1);
NSLog(@"%@-%p-%p", p2, p2, &p2);
NSLog(@"%@-%p-%p", p3, p3, &p3);

打印内容:

    <SMPerson: 0x600000710400>-0x600000710400-0x7ffee6f15088
<SMPerson: 0x600000710400>-0x600000710400-0x7ffee6f15080
<SMPerson: 0x600000710400>-0x600000710400-0x7ffee6f15078

可见,在 SMPerson 使用 alloc 方法从系统中申请开辟内存空间后 init方法并没有对内存空间做任何的处理,地址指针的创建来自于 alloc方法。如下所示:

地址.001.jpeg

注:细心的你一定注意到了,p1、p2、p3都是相差了8个字节。 这是因为,指针占内存空间大小为8字节,p1、p2、p3 都是从栈内存空间上申请的,且栈内存空间是连续的。同时,他们都指向了同一个内存地址。

那么, alloc 是如何开辟内存空间的呢?

首先,第一反应是,我们要Jump to Definition,

2241622899100_.pic_hd.jpg

结果,Xcode中并不能直接跳转后显示其底层实现,所以 并不是我们想要的。

2251622899278_.pic_hd.jpg

WX20210605-214250@2x.png

中探

接下来,我们通过三种方法来一探究竟:

方法1

既然不可以直接跳转到API文档来查看alloc的内部实现,那么我们还可以通过下 符号断点 来探寻 其实现原理。

WX20210605-212725@2x.png

接下来我们就来到此处

WX20210605-213213@2x.png

一个名为 libobjc.A.dylib 的库,至此,我们就应该要去找苹果开源的库,以寻找我们想要的答案。

点击查看苹果开源源码汇总

方法2

我们也可以直接在alloc那一行打一个断点,代码运行到此处后,按住control键 点击 step into, 接下来,就来到里这里

WX20210605-214413@2x.png 我们可以看到一个 objc_alloc 的函数方法到调用,此时,我们再下一个符号断点,同样的,我们还是找到了 libobjc.A.dylib 这个库。

WX20210605-215027@2x.png

方法3

此外,我们还是可以通过汇编来调试和查找相应的实现内容,断点依然是在alloc那一行。

Debug > Debug Workflow > Always Show Disassembly

WX20210605-215336@2x.png

找到 callq 方法调用那一行, WX20210605-215715@2x.png

接着, step into 进去, 我们找到了 objc_alloc 的调用, 之后的操作和 方法2的后续步骤一样,最终,可以找到 libobjc.A.dylib 这个库。 WX20210605-215732@2x.png

深探

下载源码 objc4-818.2

接下来对源码进行分析,

alloc方法会调用到此处

WX20210605-231454@2x.png

接着是 调用 _objc_rootAlloc

WX20210605-231517@2x.png

之后调用 到 callAlloc

WX20210605-231545@2x.png

跟着断点会来到 _objc_rootAllocWithZone

WX20210605-231647@2x.png

之后是 _class_createInstanceFromZone

此方法是重点

WX20210605-231758@2x.png

_class_createInstanceFromZone 方法中,该方法就是一个类初始化所走的流程,重点的地方有三处

第一处是:
    // 计算出开辟内存空间大小
size = cls->instanceSize(extraBytes);

内部实现如下: WX20210605-231838@2x.png 其中在计算内存空间大小时,会调用 cache.fastInstanceSize(extraBytes) 方法,

最终会调用 align16(size + extra - FAST_CACHE_ALLOC_DELTA16) 方法。 align16 的实现如下:

static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}

可见, 系统会进行 16字节 的对齐操作,也就是说,一个对象所占用的内存大小至少是16字节。

在这里 我们举个例子: size_t x = 8; 那么 align16操作后的大小计算过程如下:

    (8 + 15) & ~15;

0000 0000 0000 1000 8
0000 0000 0000 1111 15

= 0000 0000 0001 0111 23
1111 1111 1111 0000 ~15

= 0000 0000 0001 0000 16


第二处是:
    ///向系统申请开辟内存空间,返回地址指针;
obj = (id)calloc(1, size);

第三处是:
    /// 将类和指针做绑定
obj->initInstanceIsa(cls, hasCxxDtor);

总结:

所以,最后我们总结一下, alloc的底层调用流程如下:

alloc流程.001.jpeg

就是这样一个流程,系统就帮我们创建出来一个类对象。

补充

image.png

  • lldb 如何打印实力对象中成员为 double 类型的数值: e -f f -- <值>
收起阅读 »

String还有长度限制?是多少?

前言 话说Java中String是有长度限制的,听到这里很多人不禁要问,String还有长度限制?是的有,而且在JVM编译中还有规范,而且有的家人们在面试的时候也遇到了。 String 首先要知道String的长度限制我们就需要知道String是怎么存储字符串...
继续阅读 »

前言


话说Java中String是有长度限制的,听到这里很多人不禁要问,String还有长度限制?是的有,而且在JVM编译中还有规范,而且有的家人们在面试的时候也遇到了。


String


首先要知道String的长度限制我们就需要知道String是怎么存储字符串的,String其实是使用的一个char类型的数组来存储字符串中的字符的。



那么String既然是数组存储那数组会有长度的限制吗?是的有限制,但是是在有先提条件下的,我们看看String中返回length的方法。



由此我们看到返回值类型是int类型,Java中定义数组是可以给数组指定长度的,当然不指定的话默认会根据数组元素来指定:


int[] arr1 = new int[10]; // 定义一个长度为10的数组
int[] arr2 = {1,2,3,4,5}; // 那么此时数组的长度为5
复制代码

整数在java中是有限制的,我们通过源码来看看int类型对应的包装类Integer可以看到,其长度最大限制为2^31 -1,那么说明了数组的长度是0~2^31-1,那么计算一下就是(2^31-1 = 2147483647 = 4GB)



看到这我们尝试通过编码来验证一下上述观点。



以上是我通过定义字面量的形式构造的10万个字符的字符串,编译之后虚拟机提示报错,说我们的字符串长度过长,不是说好了可以存21亿个吗?为什么才10万个就报错了呢?


其实这里涉及到了JVM编译规范的限制了,其实JVM在编译时,如果我们将字符串定义成了字面量的形式,编译时JVM是会将其存放在常量池中,这时候JVM对这个常量池存储String类型做出了限制,接下来我们先看下手册是如何说的。



常量池中,每个 cp_info 项的格式必须相同,它们都以一个表示 cp_info 类型的单字节 “tag”项开头。后面 info[]项的内容 由tag 的类型所决定。



我们可以看到 String类型的表示是 CONSTANT_String ,我们来看下CONSTANT_String具体是如何定义的。



这里定义的 u2 string_index 表示的是常量池的有效索引,其类型是CONSTANT_Utf8_info 结构体表示的,这里我们需要注意的是其中定义的length我们看下面这张图。



在class文件中u2表示的是无符号数占2个字节单位,我们知道1个字节占8位,2个字节就是16位 ,那么2个字节能表示的范围就是2^16- 1 = 65535 。范中class文件格式对u1、u2的定义的解释做了一下摘要:


#这里对java虚拟机规摘要部分


##1、class文件中文件内容类型解释


定义一组私有数据类型来表示 Class 文件的内容,它们包括 u1,u2 和 u4,分别代 表了 1、2 和 4 个字节的无符号数。


每个 Class 文件都是由 8 字节为单位的字节流组成,所有的 16 位、32 位和 64 位长度的数 据将被构造成 2 个、4 个和 8 个 8 字节单位来表示。


##2、程序异常处理的有效范围解释


start_pc 和 end_pc 两项的值表明了异常处理器在 code[]数组中的有效范围。


start_pc 必须是对当前 code[]数组中某一指令的操作码的有效索引,end_pc 要 么是对当前 code[]数组中某一指令的操作码的有效索引,要么等于 code_length 的值,即当前 code[]数组的长度。start_pc 的值必须比 end_pc 小。


当程序计数器在范围[start_pc, end_pc)内时,异常处理器就将生效。即设 x 为 异常句柄的有效范围内的值,x 满足:start_pc ≤ x < end_pc


实际上,end_pc 值本身不属于异常处理器的有效范围这点属于 Java 虚拟机历史上 的一个设计缺陷:如果 Java 虚拟机中的一个方法的 code 属性的长度刚好是 65535 个字节,并且以一个 1 个字节长度的指令结束,那么这条指令将不能被异常处理器 所处理。


不过编译器可以通过限制任何方法、实例初始化方法或类初始化方法的code[]数组最大长度为 65534,这样可以间接弥补这个 BUG。



注意:这里对个人认为比较重要的点做了标记,首先第一个加粗说白了就是说数组有效范围就是【0-65565】但是第二个加粗的地方又解释了,因为虚拟机还需要1个字节的指令作为结束,所以其实真正的有效范围是【0-65564】,这里要注意这里的范围仅限编译时期,如果你是运行时拼接的字符串是可以超出这个范围的。



接下来我们通过一个小实验来测试一下我们构建一个长度为65534的字符串,看看是否就能编译通过。0期阶段汇总


首先通过一个for循环构建65534长度的字符串,在控制台打印后,我们通过自己度娘的一个在线字符统计工具计算了一下确实是65534个字符,如下:




然后我们将字符复制后以定义字面量的形式赋值给字符串,可以看到我们选择这些字符右下角显示的确实是65534,于是乎运行了一波,果然成功了。




#看到这里我们来总结一下:


##字符串有长度限制吗?是多少?


首先字符串的内容是由一个字符数组 char[] 来存储的,由于数组的长度及索引是整数,且String类中返回字符串长度的方法length() 的返回值也是int ,所以通过查看java源码中的类Integer我们可以看到Integer的最大范围是2^31 -1,由于数组是从0开始的,所以数组的最大长度可以使【0~2^31】通过计算是大概4GB。


但是通过翻阅java虚拟机手册对class文件格式的定义以及常量池中对String类型的结构体定义我们可以知道对于索引定义了u2,就是无符号占2个字节,2个字节可以表示的最大范围是2^16 -1 = 65535。


其实是65535,但是由于JVM需要1个字节表示结束指令,所以这个范围就为65534了。超出这个范围在编译时期是会报错的,但是运行时拼接或者赋值的话范围是在整形的最大范围。


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

ASM字节码插桩

ASM字节码插桩 一、什么是插桩 QQ空间曾经发布的热修复解决方案中利用Javaassist库实现向类的构造函数中插入一段代码解决CLASS_ISPREVERIFIED 问题。包括了Instant Run的实现以及参照Instant Run实现的热修复美团Ro...
继续阅读 »

ASM字节码插桩


一、什么是插桩


QQ空间曾经发布的热修复解决方案中利用Javaassist库实现向类的构造函数中插入一段代码解决CLASS_ISPREVERIFIED 问题。包括了Instant Run的实现以及参照Instant Run实现的热修复美团Robus等都利用到了插桩技术。


插桩就是将一段代码插入或者替换原本的代码。字节码插桩顾名思义就是在我们编写的源码编译成字节码(Class)后,在Android下生成dex之前修改Class文件,修改或者增强原有代码逻辑的操作。


插桩前.png


插桩后.png


我们需要查看方法执行耗时,如果每一个方法都需要自己手动去加入这些内容,当不需要时也需要一个个删去相应的代码。1个、两个方法还好,如果有10个、20个得多麻烦!所以可以利用注解来标记需要插桩的方法,结合编译后操作字节码来帮助我们自动插入,当不需要时关掉插桩即可。这种AOP思想让我们只需要关注插桩代码本身。


二、字节码操作框架


上面我们提到QQ空间使用了Javaassist来进行字节码插桩,除了Javaassist之外还有一个应用更为广泛的ASM框架同样也是字节码操作框架,Instant Run包括AspectJ就是借助ASM来实现各自的功能。


我们非常熟悉的JSON格式数据是基于文本的,我们只需要知道它的规则就能够轻松的生成、修改JSON数据。同样的Class字节码也有其自己的规则(格式)。操作JSON可以借助GSON来非常方便的生成、修改JSON数据。而字节码Class,同样可以借助Javassist/ASM来实现对其修改。



字节码操作框架的作用在于生成或者修改Class文件,因此在Android中字节码框架本身是不需要打包进入APK的,只有其生成/修改之后的Class才需要打包进入APK中。它的工作时机在上图Android打包流程中的生成Class之后,打包dex之前。


三、ASM的使用


由于ASM具有相对于Javassist更好的性能以及更高的灵活行,我们这篇文章以使用ASM为主。在真正利用到Android中之前,我们可以先在Java程序中完成对字节码的修改测试。


3.1、在AS中引入ASM


ASM可以直接从jcenter()仓库中引入,所以我们可以进入:bintray.com/进行搜索



点击图中标注的工件进入,可以看到最新的正式版本为:7.1。



因此,我们可以在AS中加入:


引入ASM.png


同时,需要注意的是:我们使用testImplementation引入,这表示我们只能在Java的单元测试中使用这个框架,对我们Android中的依赖关系没有任何影响。



AS中使用gradle的Android工程会自动创建Java单元测试与Android单元测试。测试代码分别在test与androidTest。



3.2、准备待插桩Class


test/java下面创建一个Java类:


public class InjectTest {

public static void main(String[] args) {

}
}
</pre>

由于我们操作的是字节码插桩,所以可以进入test/java下面使用javac对这个类进行编译生成对应的class文件。


javac InjectTest.java

3.3、执行插桩


因为main方法中没有任何输出代码,我们输入命令:java InjectTest执行这个Class不会有任何输出。那么我们接下来利用ASM,向main方法中插入一开始图中的记录函数执行时间的日志输出。


在单元测试中写入测试方法


<pre spellcheck="false" lang="java" cid="n37" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"> /**
* 1、准备待分析的class
*/
FileInputStream fis = new FileInputStream
("xxxxx/test/java/InjectTest.class");

/**
* 2、执行分析与插桩
*/
//class字节码的读取与分析引擎
ClassReader cr = new ClassReader(fis);
// 写出器 COMPUTE_FRAMES 自动计算所有的内容,后续操作更简单
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
//分析,处理结果写入cw EXPAND_FRAMES:栈图以扩展格式进行访问
cr.accept(new ClassAdapterVisitor(cw), ClassReader.EXPAND_FRAMES);


/**
* 3、获得结果并输出
*/
byte[] newClassBytes = cw.toByteArray();
File file = new File("xxx/test/java2/");
file.mkdirs();

FileOutputStream fos = new FileOutputStream
("xxx/test/java2/InjectTest.class");
fos.write(newClassBytes);

fos.close();</pre>

关于ASM框架本身的设计,我们这里先不讨论。上面的代码会获取上一步生成的class,然后由ASM执行完插桩之后,将结果输出到test/java2目录下。其中关键点就在于第2步中,如何进行插桩。


把class数据交给ClassReader,然后进行分析,类似于XML解析,分析结果会以事件驱动的形式告知给accept的第一个参数ClassAdapterVisitor


<pre spellcheck="false" lang="java" cid="n41" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class ClassAdapterVisitor extends ClassVisitor {

public ClassAdapterVisitor(ClassVisitor cv) {
super(Opcodes.ASM7, cv);
}

@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature,
String[] exceptions) {
System.out.println("方法:" + name + " 签名:" + desc);

MethodVisitor mv = super.visitMethod(access, name, desc, signature,
exceptions);
return new MethodAdapterVisitor(api,mv, access, name, desc);
}
}</pre>

分析结果通过ClassAdapterVisitor获得,一个类中会存在方法、注解、属性等,因此ClassReader会将调用ClassAdapterVisitor中对应的visitMethodvisitAnnotationvisitField这些visitXX方法。


我们的目的是进行函数插桩,因此重写visitMethod方法,在这个方法中我们返回一个MethodVisitor方法分析器对象。一个方法的参数、注解以及方法体需要在MethodVisitor中进行分析与处理。


<pre spellcheck="false" lang="java" cid="n45" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">package com.enjoy.asminject.example;

import com.enjoy.asminject.ASMTest;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;
import org.objectweb.asm.commons.Method;

/**
* AdviceAdapter: 子类
* 对methodVisitor进行了扩展, 能让我们更加轻松的进行方法分析
*/
public class MethodAdapterVisitor extends AdviceAdapter {

private boolean inject;

protected MethodAdapterVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor);
}


/**
* 分析方法上面的注解
* 在这里干嘛???
* <p>
* 判断当前这个方法是不是使用了injecttime,如果使用了,我们就需要对这个方法插桩
* 没使用,就不管了。
*
* @param desc
* @param visible
* @return
*/
@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
if (Type.getDescriptor(ASMTest.class).equals(desc)) {
System.out.println(desc);
inject = true;
}
return super.visitAnnotation(desc, visible);
}

private int start;

@Override
protected void onMethodEnter() {
super.onMethodEnter();
if (inject) {
//执行完了怎么办? 记录到本地变量中
invokeStatic(Type.getType("Ljava/lang/System;"),
new Method("currentTimeMillis", "()J"));

start = newLocal(Type.LONG_TYPE); //创建本地 LONG类型变量
//记录 方法执行结果给创建的本地变量
storeLocal(start);
}
}

@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
if (inject){
invokeStatic(Type.getType("Ljava/lang/System;"),
new Method("currentTimeMillis", "()J"));
int end = newLocal(Type.LONG_TYPE);
storeLocal(end);

getStatic(Type.getType("Ljava/lang/System;"),"out",Type.getType("Ljava/io" +
"/PrintStream;"));

//分配内存 并dup压入栈顶让下面的INVOKESPECIAL 知道执行谁的构造方法创建StringBuilder
newInstance(Type.getType("Ljava/lang/StringBuilder;"));
dup();
invokeConstructor(Type.getType("Ljava/lang/StringBuilder;"),new Method("<init>","()V"));


visitLdcInsn("execute:");
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("append","(Ljava/lang/String;)Ljava/lang/StringBuilder;"));

//减法
loadLocal(end);
loadLocal(start);
math(SUB,Type.LONG_TYPE);


invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("append","(J)Ljava/lang/StringBuilder;"));
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("toString","()Ljava/lang/String;"));
invokeVirtual(Type.getType("Ljava/io/PrintStream;"),new Method("println","(Ljava/lang/String;)V"));

}
}
}</pre>

MethodAdapterVisitor继承自AdviceAdapter,其实就是MethodVisitor 的子类,AdviceAdapter封装了指令插入方法,更为直观与简单。


上述代码中onMethodEnter进入一个方法时候回调,因此在这个方法中插入指令就是在整个方法最开始加入一些代码。我们需要在这个方法中插入long s = System.currentTimeMillis();。在onMethodExit中即方法最后插入输出代码。


<pre spellcheck="false" lang="java" cid="n48" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">@Override
protected void onMethodEnter() {
super.onMethodEnter();
if (inject) {
//执行完了怎么办? 记录到本地变量中
invokeStatic(Type.getType("Ljava/lang/System;"),
new Method("currentTimeMillis", "()J"));

start = newLocal(Type.LONG_TYPE); //创建本地 LONG类型变量
//记录 方法执行结果给创建的本地变量
storeLocal(start);
}
}</pre>

这里面的代码怎么写?其实就是long s = System.currentTimeMillis();这句代码的相对的指令。我们可以先写一份代码


<pre spellcheck="false" lang="java" cid="n50" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">void test(){
//插入的代码
long s = System.currentTimeMillis();
/**
* 方法实现代码....
*/
//插入的代码
long e = System.currentTimeMillis();
System.out.println("execute:"+(e-s)+" ms.");
}</pre>

然后使用javac编译成Class再使用javap -c查看字节码指令。也可以借助插件来查看,就不需要我们手动执行各种命令。


插件安装.png


安装完成之后,可以在需要插桩的类源码中点击右键:


查看字节码.png


点击ASM Bytecode Viewer之后会弹出


字节码.png


所以第20行代码:long s = System.currentTimeMillis();会包含两个指令:INVOKESTATICLSTORE


再回到onMethodEnter方法中


<pre spellcheck="false" lang="java" cid="n59" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">@Override
protected void onMethodEnter() {
super.onMethodEnter();
if (inject) {
//invokeStatic指令,调用静态方法
invokeStatic(Type.getType("Ljava/lang/System;"),
new Method("currentTimeMillis", "()J"));
//创建本地 LONG类型变量
start = newLocal(Type.LONG_TYPE);
//store指令 将方法执行结果从操作数栈存储到局部变量
storeLocal(start);
}
}</pre>

而`onMethodExit`也同样根据指令去编写代码即可。最终执行完插桩之后,我们就可以获得修改后的class数据。

四、Android中的实现


在Android中实现,我们需要考虑的第一个问题是如何获得所有的Class文件来判断是否需要插桩。Transform就是干这件事情的。


相关视频


Android项目实战 微信Matrix卡顿监控方案,函数自动埋点实践


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

Flutter 单例的实现

和谐学习!不急不躁!!我是你们的老朋友小青龙~ 前言 回顾iOS,单例的写法如下: static JXWaitingView *shared; +(JXWaitingView*)sharedInstance{ static dispatch_once_t...
继续阅读 »

和谐学习!不急不躁!!我是你们的老朋友小青龙~


前言


回顾iOS,单例的写法如下:


static JXWaitingView *shared;

+(JXWaitingView*)sharedInstance{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
shared=[[JXWaitingView alloc]initWithTitle:nil];
});
return shared;
}

其目的是通过dispatch_once来控制【初始化方法】只会执行一次,然后用static修饰的对象来接收并返回它。所以核心是只会执行一次初始化


创建单例


创建单例的案例


class Student {
String? name;
int? age;
//构造方法
Student({this.name, this.age});

// 单例方法
static Student? _dioInstance;
static Student instanceSingleStudent() {
if (_dioInstance == null) {
_dioInstance = Student();
}
return _dioInstance!;
}
}

测试单例效果


测试一


import 'package:flutter_async_programming/Student.dart';

void main() {
Student studentA = Student.instanceSingleStudent();
studentA.name = "张三";
Student studentB = Student.instanceSingleStudent();
print('studentA姓名是${studentA.name}');
print('studentB姓名是${studentB.name}');
}

运行效果


image.png


测试二


import 'package:flutter_async_programming/Student.dart';

void main() {
Student studentA = Student.instanceSingleStudent();
studentA.name = "张三";
Student studentB = Student.instanceSingleStudent();
studentB.name = "李四";
print('studentA姓名是${studentA.name}');
print('studentB姓名是${studentB.name}');
}

运行效果


image.png


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

拒绝编译等待 - 动态研发模式 ARK

iOS
背景 pod install 时间长:编译优化绝大部分任务放在了 CocoaPods 上,CocoaPods 承担了更多工作,执行时间因此变长。编译时间长:虽然现阶段绝大部分工程已经从源码编译转型成二进制编译,但编译耗时依旧在十分钟左右,且现有工程基础上已无更...
继续阅读 »



背景

iOS 业界研发模式多为 CocoaPods + Xcode + Git 的多仓组件化开发模型。为追求极致的研发体验、提升研发效率,对该研发模式进行了大量优化,但目前遇到了以下瓶颈,亟需突破:

  • pod install 时间长:编译优化绝大部分任务放在了 CocoaPods 上,CocoaPods 承担了更多工作,执行时间因此变长。

  • 编译时间长:虽然现阶段绝大部分工程已经从源码编译转型成二进制编译,但编译耗时依旧在十分钟左右,且现有工程基础上已无更好优化手段。

  • 超大型工程通病:Xcode Index 慢、爆内存、甚至卡死,链接时间长。

如何处理这些问题?

究其本质,产生这些问题的原因在于工程规模庞大。据此我们停下了对传统模式各节点的优化工作,以"缩小工程规模"为切入点,探索新型研发模式——动态研发模式 ARK。

ARK[1] 是全链路覆盖的动态研发模式,旨在保证工程体验的前提下缩小工程规模:通过基线构建的方式,提供线下研发所需物料;同时通过实时的动态库转化技术,保证本地研发仅需下载和编译开发仓库。

Show Case

动态研发模式本地研发流程图如下。接下来就以抖音产品为例,阐述如何使用 ARK 做一次本地开发。
演示基于字节跳动本地研发工具 MBox[2]

  1. 仓库下载

ARK 研发模式下,本地研发不再拉取主仓代码,取而代之的是 ARK 仓库。ARK 仓库含有与主仓对应的所有配置,一次适配接入后期不需要持续维护。

相较传统 APP 仓库动辄几个 GB 的大小,ARK 仓库贯彻了缩减代码规模这一概念。仓库仅有应用配置信息,不包含任何组件代码。ARK 仓库大小仅 2 MB,在 1 s 内可以完成仓库下载 。

在 MBox 中的使用仅需几步点击操作。首先选择要开发的产品,然后勾选 ark 模式,选择开发分支,最后点击 Create 便可以数秒完成仓库下载。

  1. 开发组件

CocoaPods 下进行组件开发一般是将组件仓库下载到本地,修改 Podfile 对应组件 A 为本地引用 pod A, :path =>'./A' ,之后进行本地开发。而在 MBox 和 ARK 的研发流程中,仅需选择要开发的组件点击 Add 便可进行本地开发。

动态研发模式 ARK 通过解析 Podfile.lock 支持了 Checkout From Commit 功能,该功能根据宿主的组件依赖信息自动拉取相应的组件版本到本地,带来便捷性的同时也保证了编译成功率。

  1. pod install

传统研发模式下 pod install 必须要经历 解析 Podfile 依赖、下载依赖、创建 Pods.xcodeproj 工程、集成 workspace 四个步骤,其中依赖解析和下载依赖两个步骤尤为耗时。

ARK 研发模式下 Podfile 中没有组件,因此依赖解析、下载依赖这两个环节耗时几乎为零。其次由于工程中仅需开发组件步骤中添加的组件,在创建 Pods 工程、集成工程这两个环节中代码规模的降低,对提升集成速度的效果非常显著。

没有依赖信息,编译、链接阶段显然不能成功。ARK 解决方案通过自研 cocoapods-ark 及配套工具链来保证编译、链接、运行的成功,其原理后续会在系列文章中介绍。

  1. 开发组件编译&调试

和传统模式一样通过 Xcode 打开工程的 xcworkspace ,即可正常开发、调试完整的应用。

工程中仅保留开发组件,但是依然有变量、函数、头文件跳转能力;参与 Index、编译的规模变小,Xcode 几乎不存在 loading 状态,大型工程也可以秒开;编译速度大幅提升。在整个动态研发流程中,通过工具链将组件从静态库转化成动态库,链接时间明显缩短。

  1. 查看全源码

ARK 工程下默认只有开发组件的源码,查看全源码是开发中的刚需。动态研发流程提供了 pod doc 异步命令实现该能力,此命令可以在开发时执行,命令执行完成后重启工程即可通过 Document Target 查看工程中其他组件源码。

pod doc 优点:

  • 支持异步和同步,执行过程中不影响本地开发。

  • 执行指令时跳过依赖解析环节,从服务端获取依赖信息,下载源码。

  • 通过 xcodegen 异步生成 Document 工程,大幅降低 pod install 时间。

  • 仅复用 pod installer 中的资源下载、缓存模块。

  • 支持仓库统一鉴权,自动跳过无权限组件仓库。

收益

体验上: 与传统模式开发流程一致,零成本切换动态研发模式。

工具上: 站在巨人的肩膀上,CocoaPods 工具链相关优化在 ARK 同样生效。

时间上: 传统研发模式中,历经各项优化后虽然能将全链路开发时间控制在 20 分钟左右,但这样的研发体验依旧不够友好。开发非常容易在这个时间间隔内被其他事情打断,良好的研发体验应该是连贯的。结合本地开发体验我们认为,一次连贯的开发体验应该将工程集成时间控制在分钟级,当前研发模式成功做到了这一点,将全链路开发时间控制在 5 分钟以内。

成功率: 成功率一直是研发效率中容易被忽视的一个指标。据不完全数据统计,集团内应用的本地全量编译成功率不足五成。一半的同学会在首次编译后执行重试。显然,对于工程新手来说就是噩梦,这意味着很长时间将在这一环节中浪费。而 ARK 从平台基线到本地工具链贯彻 Sandbox 的理念,整体上提高编译成功率。

写在最后

ARK 当前已经在字节跳动内部多业务落地使用。从初期技术方案的探索到实际落地应用,遇到了很多技术难点,也对研发模式有了新的思考。

相关技术文章将陆续分享,敬请期待。

扩展阅读

[1] ARK: https://github.com/kuperxu/KwaiTechnologyCommunication/blob/master/5.WWDC-ARK.pdf
[2] MBox: https://mp.weixin.qq.com/s/5_IlQPWnCug_f3SDrnImCw

作者:字节跳动终端技术——徐纪光
来源:https://blog.csdn.net/YZcoder/article/details/121374743


收起阅读 »

手把手带你,优化一个滚动时流畅的TableView

iOS
手把手带你,优化一个滚动时流畅的TableView这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战我的专栏iOS 底层原理探索iOS 底层原理探索 之 阶段总结意识到我的问题平时使用手机的时间不算少,每天阅读新闻的时候会感觉到新闻类的app优化的还是...
继续阅读 »

手把手带你,优化一个滚动时流畅的TableView

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战


我的专栏

  1. iOS 底层原理探索
  2. iOS 底层原理探索 之 阶段总结

意识到我的问题

平时使用手机的时间不算少,每天阅读新闻的时候会感觉到新闻类的app优化的还是很好的,TableView的Cell滚动的时候不会去加载显示图片内容,当一次滑动结束之后,Cell上的新闻图片便开始逐个的加载显示出来,所以整个滑动的过程是很流畅的。这中体验也是相当nice的。

我最开始的做法

开发中TableView的使用是非常值频繁的,当TableViewCell上需要加载图片的时候,是一件比较头疼的事。因为,用户一边滑动TableView,TableView需要一边从网络获取图片。之前的操作都是放在 cellForRowAtIndexPath 中来处理,这就导致用户在滑动TableView的时候,会特别的卡(尤其是滑动特别快时),而且,手机的CPU使用率也会飙的非常的高。对于用户来说,这显然是一个十分糟糕的体验。

糟糕的图片显示 代码

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

ImageTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
cell.index = indexPath;

NSMutableDictionary *info = [self.dataSource objectAtIndex:cell.index.row];

NSString *url = [info objectForKey: @"img" ];
NSData *iData = [NSData dataWithContentsOfURL:[NSURL URLWithString: url ]];
cell.img.image = [UIImage imageWithData:iData];
cell.typeL.text = [NSString stringWithFormat:@"%ld-%ld", cell.index.section, cell.index.row];

return cell;
}

糟糕的手机CPU飙升率

未命名.gif

糟糕的用户滑动体验

未命名1.gif

不只是用户,对于开发这来讲,这也是不可以接受的体验。

平时接触并使用的app也非常的多,发现他们多处理方式就是,当用户滑动列表的时候,不再加载图片,等用户的滑动结束之后,会开始逐一的加载图片。这是非常好的优化思路,减轻了CPU的负担,也不会基本不会让用户感觉到页面滚动时候的卡顿。这也就是最开始我描述的我看新闻app的使用体验。

收到这个思路的启发,我们开始着手将上面糟糕的体验作一下优化吧。

总结思路开启优化之路

那么,带着这个优化思路,我开始了对于这个TableView 的优化。

  • 首先,我们只加载当前用户可以看到的cell上的图片。
  • 其次,我们一次只加载一张图片。

要完成以上两点,图片的加载显示就不能在cellForRowAtIndexPath中完成,我们要定义并实现一个图片的加载显示方法,以便在合适的时机,调用刷新内容显示。

loadSeeImage 加载图片的优化

#pragma mark load Images
- (void)loadSeeImage {

//记录本次加载的几张图片
NSInteger loadC = 0;

// 用户可以看见的cells
NSArray *cells = [self.imageTableView visibleCells];

// 调度组
dispatch_group_t group = dispatch_group_create();

for (int i = 0; i < cells.count; i++) {

ImageTableViewCell *cell = [cells objectAtIndex:i];

NSMutableDictionary *info = [self.dataSource objectAtIndex:cell.index.row];
NSString *url = [info objectForKey: @"img" ];

NSString *data = [info objectForKey:@"data"];

if ([data isKindOfClass:[NSData class]]) {


}else {

// 添加调度则到我们的串行队列中去
dispatch_group_async(group, self.loadQueue, ^{

NSData *iData = [NSData dataWithContentsOfURL:[NSURL URLWithString: url ]];
NSLog(@" load image %ld-%ld ", cell.index.section, cell.index.row);
if (iData) {
// 缓存
[info setValue:@"1" forKey:@"isload"];
[info setValue:iData forKey:@"data"];
}
NSString *isload = [info objectForKey:@"isload"];

if ([isload isEqualToString:@"0"]) {

dispatch_async(dispatch_get_main_queue(), ^{

cell.img.image = [UIImage imageNamed:@""];
}); }else {

if (iData) {

dispatch_async(dispatch_get_main_queue(), ^{
//显示加载后的图片
cell.img.image = [UIImage imageWithData:iData];
});
}
}

});

if (i == cells.count - 1) {

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 全部加载完毕的通知
NSLog(@"load finished");
});
}

loadC += 1;
}
}

NSLog(@"本次加载了 %ld 张图片", loadC);
}

其次就是 loadSeeImage 调用时机的处理,我们要做到用户在滑动列表之后加载,就是在下面两处加载:

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView   {  

[self loadSeeImage];
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {

if (scrollView.isDragging || scrollView.isDecelerating || scrollView.isTracking) {
return;
}
[self loadSeeImage];
}

当然,首次进入页面,列表数据加载完毕后,我们也要加载一次图片的哦。 好的下面看下优化后的结果:

优化xcode.gif

优化phone.gif

CPU占用率比之前最高的时候降低了一半多,app在滑动的时候也没有明显卡顿的地方。 完美。

收起阅读 »

面向 JavaScript 开发人员的 5 大物联网库

最近元宇宙的概念火遍互联网,自 Facebook 改名 Meta 以来,国内外越来越多的企业纷纷加入到布局元宇宙的行列。元宇宙之所以强势进入人们视野,与其底层技术的进步有着莫大的关系,包括AR/VR、云计算、物联网、5G、人工智能、数字孪生等等。其中,5G作为...
继续阅读 »


最近元宇宙的概念火遍互联网,自 Facebook 改名 Meta 以来,国内外越来越多的企业纷纷加入到布局元宇宙的行列。元宇宙之所以强势进入人们视野,与其底层技术的进步有着莫大的关系,包括AR/VR、云计算、物联网、5G、人工智能、数字孪生等等。其中,5G作为重要的连接基础,是元宇宙场景得以实现的关键。元宇宙将汇集游戏引擎、AR可穿戴设备、VR、现实世界数据集和不断发展的物联网。

物联网(英语:InternetofThings,简称 IoT)是一种计算设备、机器、数码机器之间相互联系的系统,它拥有一种统一的统一识别代码(UID),并且能够在网络上传送数据,不需要人与人、或人与设备之间的交互。

作为一个前端工程师(JavaScript工程师),似乎觉得这一切有点模式,其实不然,现代 JavaScript 的可以使用的场景越来越多,包括物联网,在本文中,将介绍可以在 JavaScript 代码中用于连接设备的 5 个脚本库。

1. Cylon.js

官方网站: https://cylonjs.com/

Cylon.js 是用于机器人、物理计算和物联网 (IoT) 的流行 JavaScript 框架之一。不仅仅是一个“物联网”库,它还是一个完整的机器人框架,支持超过 43 个不同的平台,这是与机器连接的 43 种不同的地方或方式,目前支持的机器人和物理计算系统及软件平台有Arduino、Beaglebone Black、BLE、Disispark、Intel Galileo and Edison、Intel IoT Analytics、OpenCV、Octobl、Raspberry Pi、Salesforce等。

可以使用 Cylon.js 连接到关键字并侦听它或 Arduino 板发送的事件,或者提供一个 HTTP API 接口并通过那里获取数据(它们也支持 socket.ioMQTT)。想通过 JavaScript 控制无人机吗?这并非不可以,首先需要安装:

npm install cylon cylon-firmata cylon-gpio cylon-i2c

然后运行一个这样的小脚本, 参考文章:

npm install cylon cylon-ardrone

然后运行脚本:

const Cylon = require("cylon");

Cylon.robot({
  connections: {
      ardrone: { adaptor: "ardrone", port: "192.168.1.1" },
  },

  devices: {
      drone: { driver: "ardrone" },
  },

  work: function (my) {
      my.drone.takeoff();
      after((10).seconds(), function () {
          my.drone.land();
      });
      after((15).seconds(), function () {
          my.drone.stop();
      });
  },
}).start();

如果有设备可以试试。 Cylon.js 的工作方式是允许其他人通过插件的方式提供连接器,这意味着这个库提供的功能没有限制。最重要的是,文档本身非常详细,写得很好,完整的代码示例。

2. IoT.js

官方网站: https://iotjs.net/

IoT.js 是一个用 JavaScript 编写的物联网 (IoT) 框架。它旨在基于网络技术在物联网世界中提供一个可互操作的服务平台。

如果希望在一个连接的设备中执行一些物联网(而不是在一个强大的、充满资源的服务器中的接收端),那么可能需要针对该环境进行优化。这个 IoT 框架运行在 JerryScript 引擎之上, JerryScript 引擎是一个针对小型设备优化的 JavaScript 运行时。这意味着,虽然无法使用最先进的 JS 的全部功能,但确实可以使用:

  • 完全支持 ECMAScript 5.1 语法。

  • 低内存消耗优化

  • 能够将 JS 代码预编译为字节码

但是,兼容平台的数量没有 Cylon.js 多,而 IoT.js 只兼容:

关于他们的文档,这应该是衡量一个库有多好的标准之一。他们有一些基本的例子和入门指南。但可能就是这样了。考虑到 IoT.js 是一个底层的硬件接口,现在看起来它希望开发人员已经有使用其他产品的经验,而不是针对JS开发人员寻求进入物联网。

3. Johnny-Five

官方网站: http://johnny-five.io/

Johnny Five 是流行的 JavaScript 机器人和物联网平台之一。由 Bocoup 于 2012 年开发的 Johnny Five 一个开源的、基于 Firmata 协议的物联网和机器人编程框架,是 JavaScript 开发人员可用的最古老的机器人和物联网平台之一,从那时起,它的功能和兼容性都在不断增长。

Johnny Five 支持 Arduino(所有型号)、Electric Imp、Beagle Bone、Intel Galileo & Edison、Raspberry Pi 等。该平台可轻松与流行的应用程序库(如 Express.js 和 Socket.io)以及物联网框架(如 Octoblu)结合使用。

他们的文档非常详细,充满了关于硬件连接的示例和图表,这是一个很好的学习资源。

4. NodeRed

官方网站: https://nodered.org/

NodeRed 是建立在 Node.js 之上,是一个基于流的编程工具,最初由 IBM 的新兴技术服务团队开发,现在是 JS 基金会的一部分。该平台允许在部署之前从浏览器以图形方式设置数据流和工作流。在理想的情况下,不需要编写任何代码,也许设置一些平台凭据。 NodeRed 还充当和其他人共享他们创建的流程的中心化平台,这是防止每次都重新创建轮子的好方法,即使没有真正编写代码。

5. Zetta

官方网站: https://www.zettajs.org/

ZettaJS 是一个基于 Node.js 构建的开源平台,用于创建跨地理分布式计算机和云运行的物联网服务器。是另一种通过 JavaScript 与远程设备交互的方式。这里的主要区别在于 ZettaJS 的目标是将每个设备都变成一个 API,这是将 IoT 泛化为一个通用概念的好方法。如今,设备及其接口的数量正在爆炸增长,但没有对其进行规范控制。 ZettaJS 正试图在这方面进行改进,通过非常直观的编码方式,可以简单地为设备安装驱动程序,并在其中启用公共接口,并通过代码与它们交互。

6. 总结

通过上面介绍,JavaScript 不仅限于浏览器,甚至不限于基于 API 的后端开发,还可以随心所欲地从设备中提取数据或从设备中提取数据,并使用几乎完全相同的语言来控制它。

作者:天行无忌
来源:https://blog.51cto.com/devpoint/4762760

收起阅读 »

给团队做个分享,用30张图带你快速了解TypeScript

正文30张脑图常见的基本类型我们知道TS是JS的超集,那我们先从几种JS中常见的数据类型说起,当然这些类型在TS中都有相应的,如下:特殊类型除了一些在JS中常见的类型,也还有一些TS所特有的类型类型断言和类型守卫如何在运行时需要保证和检测来自其他地方的数据也符...
继续阅读 »

正文

30张脑图

常见的基本类型

我们知道TSJS的超集,那我们先从几种JS中常见的数据类型说起,当然这些类型在TS中都有相应的,如下:

1常见的基本类型.png

特殊类型

除了一些在JS中常见的类型,也还有一些TS所特有的类型

2特殊类型.png

类型断言和类型守卫

如何在运行时需要保证和检测来自其他地方的数据也符合我们的要求,这就需要用到断言,而断言需要类型守卫

3类型断言.png

接口

接口本身只是一种规范,里头定义了一些必须有的属性或者方法,接口可以用于规范functionclass或者constructor,只是规则有点区别

4TS中的接口.png

类和修饰符

JS一样,类class出现的目的,其实就是把一些相关的东西放在一起,方便管理

TS主要也是通过class关键字来定义一个类,并且它还提供了3个修饰符

5类和修饰符.png

类的继承和抽象类

TS中的继承ES6中的类的继承极其相识,子类可以通过extends关键字继承一个类

但是它还有抽象类的概念,而且抽象类作为基类,不能new

6.0类的继承和抽象类.png

泛型

将泛型理解为宽泛的类型,它通常用于类和函数

但不管是用于类还是用于函数,核心思想都是:把类型当一种特殊的参数传入进去

7泛型.png

类型推断

TS中是有类型推论的,即在有些没有明确指出类型的地方,类型推论会帮助提供类型

8类型推断.png

函数类型

为了让我们更容易使用,TS为函数添加了类型等

9函数.png

数字枚举和字符串枚举

枚举的好处是,我们可以定义一些带名字的常量,而且可以清晰地表达意图或创建一组有区别的用例

TS支持数字的和基于字符串的枚举

10枚举.png

类型兼容性

TS里的类型兼容性是基于结构子类型的 11类型兼容性.png

联合类型和交叉类型

补充两个TS的类型:联合类型和交叉类型

12联合类型和交叉类型.png

for..of和for..in

TS也支持for..offor..in,但你知道他们两个主要的区别吗

13forin和forof.png

模块

TS的模块化沿用了JS模块的概念,模块是在自身的作用域中执行,在一个模块里的变量,函数,类等等在模块外部是不可见的,除非你明确地使用export形式之一导出它们

14模块.png

命名空间的使用

使用命名空间的方式,其实非常简单,格式如下: namespace X {}

15命名空间的使用.png

解决单个命名空间过大的问题

16解决单个命名空间过大的问题.png

简化命名空间

要简化命名空间,核心就是给常用的对象起一个短的名字

TS中使用import为指定的符号创建一个别名,格式大概是:import q = x.y.z

17简化命名空间.png

规避2个TS中命名空间和模块的陷阱

18陷阱.png

模块解析流程

模块解析是指编译器在查找导入模块内容时所遵循的流程

流程大致如下:

image.png

相对和非相对模块导入

相对和非相对模块导入主要有以下两点不同

image.png

Classic模块解析策略

TS的模块解析策略,其中的一种就叫Classic

21Classic模块解析策略.png

Node.js模块解析过程

为什么要说Node.js模块解析过程,其实是为了讲TS的另一种模块解析策略做铺垫---Node模块解析策略。

因为Node模块解析策略就是一种试图在运行时模仿Node.js模块解析的策略

22Node.js的模块解析过程.png

Node模块解析策略

Node模块解析策略模仿Node.js运行时的解析策略来在编译阶段定位模块定义文件的模块解析的策略,但是跟Node.js会有点区别

23Node模块解析策略.png

声明合并之接口合并

声明合并指的就是编译器会针对同名的声明合并为一个声明

声明合并包括接口合并,接口的合并需要区分接口里面的成员有函数成员和非函数成员,两者有差异

24接口合并.png

合并命名空间

命名空间的合并需要分两种情况:一是同名的命名空间之间的合并,二是命名空间和其他类型的合并

25合并命名空间.png

JSX模式

TS具有三种JSX模式:preservereactreact-native

26JSX.png

三斜线指令

三斜线指令其实上面有讲过,像/// <reference>

它的格式就是三条斜线后面跟一个标签

27三斜线指令.png


作者:LBJ
链接:https://juejin.cn/post/7036266588227502093

收起阅读 »

我去!爬虫遇到字体反爬,哭了

今天准备爬取某某点评店铺信息时,遇到了『字体』反爬。比如这样的: 还有这样的: 可以看到这些字体已经被加密(反爬) 竟然遇到这种情况,那辰哥就带大家如何去解决这类反爬(字体反爬类) 01 网页分析在开始分析反爬之前,先简单的介绍一下背景(爬取的网页) 辰...
继续阅读 »

今天准备爬取某某点评店铺信息时,遇到了『字体』反爬。比如这样的:


img

还有这样的:


img

可以看到这些字体已经被加密反爬


竟然遇到这种情况,那辰哥就带大家如何去解决这类反爬(字体反爬类


01 网页分析

在开始分析反爬之前,先简单的介绍一下背景(爬取的网页)


img

辰哥爬取的某某点评的店铺信息。一开始查看网页源码是这样的


img

这种什么也看不到,咱们换另一种方式:通过程序直接把整个网页源代码保存下来


img

获取到的网页源码如下:


img

比如这里看到评论数(4位数)都有对应着一个编号(相同的数字编号相同),应该是对应着网站的字体库


下一步,我们需要找到这个网站的字体库。


02 获取字体库

这里的字体库建议在目标网站里面去获取,因为不同的网站的字体库是不一样,导致解码还原的字体也会不一样。


1、抓包获取字体库


img

在浏览器network里面可以看到一共有三种字体库。(三种字体库各有不同的妙用,后面会有解释


img

把字体库链接复制在浏览器里面打开,就可以把字体库下载到本地。


2、查看字体库


这里使用FontCreator的工具查看字体库。


下载地址:


https://www.high-logic.com/font-editor/fontcreator/download

这里需要注册,邮箱验证才能下载,不过辰哥已经下载了,可以在公众号回复:FC,获取安装包。


安装之后,把刚刚下载的字体库在FontCreator中打开


img

可以看到字体的内容以及对应的编号


比如数字7对应F399数字8对应F572 ,咱们在原网页和源码对比,是否如此???


img

可以看到,真是一模一样对应着解码就可以还原字体。


3、为什么会有三个字体库


img

在查看加密字体的CSS样式时,方式有css内容是这样的


img

字体库1:d35c3812.woff 对应解码class为 shopNum


字体库2:084c9fff.woff 对应解码class为 reviewTag和address


字体库3:73f5e6f3.woff 对应解码class为 tagName


也就是说,字体所属的不同class标签,对应的解密字体库是不一样的,辰哥这里不得不说一句:太鸡贼了


img

咱们这里获取的评论数,clas为shopNum,需要用到字体库d35c3812.woff


03 代码实现解密

1、加载字体库


既然我们已经知道了字体反爬的原理,那么我们就可以开始编程实现解密还原。


加载字体库的Python库包是:fontTools ,安装命令如下:


pip install fontTools

img

将字体库的内容对应关系保存为xml格式


img

code和name是一一对应关系


img

img

可以看到网页源码中的编号后四位对应着字体库的编号。


因此我们可以建立应该字体对应集合


img

建立好映射关系好,到网页源码中去进行替换


img

img

这样我们就成功的将字体反爬处理完毕。后面提取内容大家基本都没问题。


2、完整代码


img

输出结果:


img

可以看到加密的数字全部都还原了。


04 小结

辰哥在本文中主要讲解了如此处理字体反爬问题,并以某某点评为例去实战演示分析。辰哥在文中处理的数字类型,大家可以尝试去试试中文如何解决。


作者:Python研究者
来源:https://juejin.cn/post/6970933428145356831

收起阅读 »

js实现放大镜

借助宽高等比例放大的两张图片,结合js中鼠标偏移量、元素偏移量、元素自身宽高等属性完成;左侧遮罩移动Xpx,右侧大图移动X*倍数px;其余部分就是用小学数学算一下就OK了。JS // 获取小图和遮罩、大图、大盒子    var small ...
继续阅读 »



先看效果图

实现原理

借助宽高等比例放大的两张图片,结合js中鼠标偏移量、元素偏移量、元素自身宽高等属性完成;左侧遮罩移动Xpx,右侧大图移动X*倍数px;其余部分就是用小学数学算一下就OK了。

HTML和CSS

 <div class="wrap">
   
   <div id="small">
     <img src="img/1.jpg" alt="" >
     <div id="mark">div>
   div>
   
   <div id="big">
     <img src="img/2.jpg" alt="" id="bigimg">
   div>
 div>
* {
    margin: 0;
    padding: 0;
  }
  .wrap {
    width: 1500px;
    margin: 100px auto;
  }

  #small {
    width: 432px;
    height: 768px;
    float: left;
    position: relative;
  }

  #big {
    /* background-color: seagreen; */
    width: 768px;
    height: 768px;
    float: left;
    /* 超出取景框的部分隐藏 */
    overflow: hidden;
    margin-left: 20px;
    position: relative;
    display: none;
  }

  #bigimg {
    /* width: 864px; */
    position: absolute;
    left: 0;
    top: 0;
  }

  #mark {
    width: 220px;
    height: 220px;
    background-color: #fff;
    opacity: .5;
    position: absolute;
    left: 0;
    top: 0;
    /* 鼠标箭头样式 */
    cursor: move;
    display: none;
  }

JS

 // 获取小图和遮罩、大图、大盒子
   var small = document.getElementById("small")
   var mark = document.getElementById("mark")
   var big = document.getElementById("big")
   var bigimg = document.getElementById("bigimg")
   // 在小图区域内获取鼠标移动事件;遮罩跟随鼠标移动
   small.onmousemove = function (e) {
     // 得到遮罩相对于小图的偏移量(鼠标所在坐标-小图相对于body的偏移-遮罩本身宽度或高度的一半)
     var s_left = e.pageX - mark.offsetWidth / 2 - small.offsetLeft
     var s_top = e.pageY - mark.offsetHeight / 2 - small.offsetTop
     // 遮罩仅可以在小图内移动,所以需要计算遮罩偏移量的临界值(相对于小图的值)
     var max_left = small.offsetWidth - mark.offsetWidth;
     var max_top = small.offsetHeight - mark.offsetHeight;
     // 遮罩移动右侧大图也跟随移动(遮罩每移动1px,图片需要向相反对的方向移动n倍的距离)
     var n = big.offsetWidth / mark.offsetWidth
     // 遮罩跟随鼠标移动前判断:遮罩相对于小图的偏移量不能超出范围,超出范围要重新赋值(临界值在上边已经计算完成:max_left和max_top)
     // 判断水平边界
     if (s_left < 0) {
       s_left = 0
    } else if (s_left > max_left) {
       s_left = max_left
    }
     //判断垂直边界
     if (s_top < 0) {
       s_top = 0
    } else if (s_top > max_top) {
       s_top = max_top
    }
     // 给遮罩left和top赋值(动态的?因为e.pageX和e.pageY为变化的量),动起来!
     mark.style.left = s_left + "px";
     mark.style.top = s_top + "px";
     // 计算大图移动的距离
     var levelx = -n * s_left;
     var verticaly = -n * s_top;
     // 让图片动起来
     bigimg.style.left = levelx + "px";
     bigimg.style.top = verticaly + "px";
  }
   // 鼠标移入小图内才会显示遮罩和跟随移动样式,移出小图后消失
   small.onmouseenter = function () {
     mark.style.display = "block"
     big.style.display= "block"
  }
   small.onmouseleave = function () {
     mark.style.display = "none"
     big.style.display= "none"
  }

总结

  • 鼠标焦点一旦动起来,它的偏移量就是动态的;父元素和子元素加上定位后,通过动态改变某个元素的lefttop值来实现“动”的效果。

  • 大图/小图=放大镜(遮罩)/取景框

  • 两张图片一定要等比例缩放

作者:Onion韩
来源:https://juejin.cn/post/7030963292818374670

收起阅读 »

从谷歌一行代码学到的姿势

网上很流行的一行代码,据说是谷歌工程师写的,它的作用是给页面所有元素增加一个随机颜色的外边框。[].forEach.call($$("*"),function(a){a.style.outline="1px solid #"+(~~(Math.random()...
继续阅读 »

网上很流行的一行代码,据说是谷歌工程师写的,它的作用是给页面所有元素增加一个随机颜色的外边框

[].forEach.call($$("*"),function(a){a.style.outline="1px solid #"+(~~(Math.random()*(1<<24))).toString(16)})

运行效果如下图:

这个代码虽然只有一行,但是包含的知识点不少,网上有很多解析。我也说下自己的理解,然后最后推荐在实务中使用TreeWalker对象进行遍历。

我的理解其中主要包含如下4个知识点:

1. [].forEach.call
2. $$("*")
3. a.style.outline
4. (~~(Math.random()*(1<<24))).toString(16)

1 [].forEach.call

1.1 [].forEach

forEach是数组遍历的一个方法,接收一个函数参数用来处理每一个遍历的元素,常规的使用姿势是:

let arr = [3, 5, 8];
arr.forEach((item) => {
console.log(item);
})
// 控制台输出:
// 3
// 5
// 8

那么下面的写法:

[].forEach

只是为了得到 forEach 这个方法,这个方法是定义都在Array.prototype上的方法,[] 表示空数组,可以访问到数组原型对象上的方法。

得到 forEach 这个方法后,就可以通过 call 发起调用。

1.2 call

call函数用来调用一个函数,和普通调用不同,call调用可以修改函数内this指向。

常规调用函数的姿势:

let object1 = {
id: 1,
printId() {
console.log(this.id)
}
}
object1.printId();
// 控制台输出:
// 1

因为是正常调用,方法内的this指向object1对象,所以上例输出1。

使用call调用printId方法,并传入另外一个对象object2:

let object2 = {
id: 2
}
object1.printId.call(object2);
// 控制台输出:
// 2

这里使用call调用object1.printId函数,传入了object2对象,那么printId函数内的this就是指向object2这个对象,所以结果输出2。

1.3 综合分析

综合来看:

[].forEach.call( $$("*"), function(a){} )

这行代码的意思就是遍历如下对象:

$$("*") 

然后用如下方法处理每个元素:

function(a){}

其中,a就是遍历的的每一个元素。

那么

$$("*") 

指什么呢?我们接着往后看。

2 $$("*")

这个写法用来获取页面所有元素,相当于

document.querySelectorAll('*')

只是

$$("*") 

只能在浏览器开发控制台内使用,这个是浏览器开发控制台提供出来的预定义API,至于为什么,大家可以参考底部的参考文章。

3 a.style.outline

设置元素边框,估计很多人都知道,但是设置外边框就比较少人了解了,外边框的效果和边框类似,唯一不同的点是外边框盒子模型的算式,仅仅做装饰使用。

<style type="text/css">
#swiper {
width: 100px;
height: 100px;
outline: 10px solid;
}
style>

<div id="swiper">div>

运行效果:

div元素实际的宽高还是100 * 100,如果把outline改成border,那么div元素的实际宽高就是120 * 120,因为要加上border的宽度。

外边框设置的最大作用就是:

可以设置元素边框效果,但是不影响页面布局。

4 (~~(Math.random()*(1<<24))).toString(16)

这个代码从结果是得到一个16进制的颜色值,但是为什么能得到呢?

16进制的颜色值:81f262

4.1 Math.random()

这个容易理解,就是随机 [0, 1) 的小数。

4.2 1<<24

这个表示1左移24位,二进制表示如下所示:

1 0000 0000 0000 0000 0000 0000  

十进制就是表示:

2^24

那么

Math.random() * (1<<24)

就会得到如下范围的一个随机浮点数:

[0, 2^24) 

4.3 两次按位取反

因为Math.random()得到是一个小数,所以两次按位取反就是为了过滤掉小数部分,最后得到整数。

所以

(~~(Math.random()*(1<<24)))

就会得到如下范围的一个随机整数:

[0, 2^24) 

4.4 转成字符串toString(16)

最后就是把上面得到的数字转成16进制,我们知道toString()是用来把相关的对象转成字符串的,它可以接收一个进制参数,转成不同的进制,默认是转成10进制。

对象.toString(2); // 转成2进制
对象.toString(8); // 转成8进制
对象.toString(10); // 转成10进制
对象.toString(16); // 转成16进制

上面的得到的随机整数用二进制表示就是:

0000 0000 0000 0000 0000 0000  

1111 1111 1111 1111 1111 1111

那么2进制转成16进制,是不是就是每4位转一个?

最终是不是就得到一个6个长度的16进制数了?

这个字符串加上#是不是就是16进制的颜色值了?

形如:

#ac83ce
#b74384
等等...

实务应用

虽然上面的代码简短,并且知识含量也很高,但是在实务中如果要遍历元素,我并不建议使用这样的方式。

主要原因是两个:

1. $$("*") 只在开发控制台可以用,正常项目代码中不能用。
2. 选中所有元素再遍历,性能低。

如果实务中要遍历元素,建议是用 TreeWalker。querySelectorAll是一次性获取所有元素然后遍历,TreeWalker是迭代器的方式,性能上 TreeWalker 更优,另外 TreeWalker 还支持各种过滤。

参考如下示例:

// 实例化 TreeWalker 对象
let walker = document.createTreeWalker(
document.documentElement,
NodeFilter.SHOW_ELEMENT
);
// 遍历
let node = walker.nextNode();
while (node !== null) {
node.style.outline = "1px solid #" + (~~(Math.random() * (1 << 24))).toString(16);
node = walker.nextNode();
}

虽然代码更多,当时性能更好,并且支持各种过滤等,功能也更加强大。

如果大家有学到新姿势,麻烦帮忙点个赞,谢谢。欢迎大家留言讨论。

参考资料

JavaScript中的$$(*)代表什么和$选择器的由来:ourjs.com/detail/54ab…

querySelectorAll vs NodeIterator vs TreeWalker:stackoverflow.com/questions/6…

作者:晴空闲云
来源:https://juejin.cn/post/7034777643014684703

收起阅读 »

现在实现倒计时都这么卷了吗?

但是在校准时间的过程中,为了快速追赶落后的时间,时间跳动太快了,导致体验不太好,体感上感觉这时间不准呀,因此我再在那基础上再优化了一版 为求实现一版超准确!超平稳!性能极好!体验极佳的倒计时 旧版的功能实现代码 const totalDuration = 10...
继续阅读 »

但是在校准时间的过程中,为了快速追赶落后的时间,时间跳动太快了,导致体验不太好,体感上感觉这时间不准呀,因此我再在那基础上再优化了一版


为求实现一版超准确!超平稳!性能极好!体验极佳的倒计时


旧版的功能实现代码


const totalDuration = 10 * 1000;
let requestRef = null;
let startTime;
let prevEndTime;
let prevTime;
let currentCount = totalDuration;
let endTime;
let timeDifferance = 0; // 每1s倒计时偏差值,单位ms
let interval = 1000;
let nextTime = interval;

setInterval(() => {
let n = 0;
while (n++ < 1000000000);
}, 0);

const animate = (timestamp) => {
if (prevTime !== undefined) {
const deltaTime = timestamp - prevTime;
if (deltaTime >= nextTime) {
prevTime = timestamp;
prevEndTime = endTime;
endTime = new Date().getTime();
currentCount = currentCount - 1000;
console.log("currentCount: ", currentCount / 1000);
timeDifferance = endTime - startTime - (totalDuration - currentCount);
console.log(timeDifferance);
nextTime = interval - timeDifferance;
// 慢太多了,就立刻执行下一个循环
if (nextTime < 0) {
nextTime = 0;
}
console.log(`执行下一次渲染的时间是:${nextTime}ms`);
if (currentCount <= 0) {
currentCount = 0;
cancelAnimationFrame(requestRef);
console.log(`累计偏差值: ${endTime - startTime - totalDuration}ms`);
return;
}
}
} else {
startTime = new Date().getTime();
prevTime = timestamp;
endTime = new Date().getTime();
}
requestRef = requestAnimationFrame(animate);
};

requestRef = requestAnimationFrame(animate);


然后有个细小的问题在于这段代码


// 慢太多了,就立刻执行下一个循环
if (nextTime < 0) {
nextTime = 0;
}

问题在于,假如遇到线程阻塞的情况,出现了倒计时落后情况严重,假设3s,我这里设置下一个循环是0s,然后现在倒计时当前15s,就会看到快速倒计时到12s,产品同学说你这倒计时还怎么加速了呀


这倒计时加速像极了职业生涯结束在加速倒计时一样,瑟瑟发抖的我立刻赶紧修复一下


其实很简单,就是把这个临近值0设置接近每次循环的时间数即可,那么其实是看不出来每次是有在稍微加速/减速的,这里每次循环的时间数是1s,那么我们可以将上面这段代码修改下,把以前立刻就追赶描述的操作,放缓一下追赶的脚步,以此优化用户体验


例如以前追赶2s3s~4s内立刻追赶上,那么波动是很明显的,但是如果把2s的落后秒数,平躺到接下来要倒计时的1min里,每次大概追赶30ms,那是看不出来滴


// 慢到一定临界点,比正常循环的时间数稍微慢点,再执行下一个循环
if (nextTime < 900) {
nextTime = 900;
}

这里我设置落后太多时,每秒追赶100ms,假如落后2s20s后就能追赶回来啦,而且看不出明显波动,时间又是被校验准确的,得到了产品同学的好评!


虽然修改很小,但是也是反复思考得到的~如果对时间要求比较严格,而且倒计时时间范围比较小,来不及把差距平摊到这么大的时间段,可建议让后端同学定时推送最新的倒计时给前端来校验时间准确性,这就万无一失啦


结语


以上是我使用requestAnimationFrame实现倒计时功能反复雕琢的心得,希望能对大家有帮助~如果能获得一个小小的赞作为鼓励会十分感激!!


作者:一只凤梨
链接:https://juejin.cn/post/7026735190634414087

收起阅读 »

中高级前端不一定了解的setTimeout | 网易实践小总结

setTimeout的创建和执行 我们知道setTimeout是一个延时器,它会在规定的时间后延迟执行回调函数,这篇文章就来说说setTimeout它是怎么执行的。 首先我们知道消息队列是用来存储宏任务的,并且主线程会按照顺序取出队列里的任务依次执行,所以为了...
继续阅读 »

setTimeout的创建和执行


我们知道setTimeout是一个延时器,它会在规定的时间后延迟执行回调函数,这篇文章就来说说setTimeout它是怎么执行的。


首先我们知道消息队列是用来存储宏任务的,并且主线程会按照顺序取出队列里的任务依次执行,所以为了保证setTimeout能够在规定的时间内执行,setTimeout创建的任务不会被添加到消息队列里,与此同时,浏览器还维护了另外一个队列叫做延迟消息队列,该队列就是用来存放延迟任务,setTimeout创建的任务会被存放于此,同时它会被记住创建时间,延迟执行时间。


然后我们看下具体例子:


setTimeout(function showName() { console.log('showName') }, 1000)
setTimeout(function showName() { console.log('showName1') }, 1000)
console.log('martincai')

以上例子执行是这样:



  • 1.从消息队列中取出宏任务进行执行(首次任务直接执行)

  • 2.执行setTimeout,此时会创建一个延迟任务,延迟任务的回调函数是showName,发起时间是当前的时间,延迟时间是第二个参数1000ms,然后该延迟任务会被推入到延迟任务队列

  • 3.执行console.log('martincai')代码

  • 4.从延迟队列里去筛选所有已过期任务(当前时间 >= 发起时间 + 延迟时间),然后依次执行


所以我们可以看到showName和showName1同时执行,原因是两个延迟任务都已经过期


循环源码:


void MainTherad(){
for(;;){
// 执行消息队列中的任务
Task task = task_queue.takeTask();
ProcessTask(task);

// 执行延迟队列中的任务
ProcessDelayTask()

if(!keep_running) // 如果设置了退出标志,那么直接退出线程循环
break;
}
}

删除延迟任务


clearTimeout是windows下提供的原生方法,用于删除特定的setTimeout的延迟任务,我们在定义setTimeout的时候返回值就是当前任务的唯一id值,那么clearTimeout就会拿着id在延迟消息队列里查找对应的任务,将其踢出队列即可


setTimeout的几个注意点:



  1. setTimeout会受到消息队列里的宏任务的执行时间影响,上面我们可以看到延迟消息队列的任务会在消息队列的弹出的当前任务执行完之后再执行,所以当前任务的执行时间会阻碍到setTimeout的延迟任务的执行时间


  function showName() {
setTimeout(function show() {
console.log('show')
}, 0)
for (let i = 0; i <= 5000; i++) {}
}
showName()

这里去执行一遍可以发现setTimeout并不是在0ms左右执行,中间会有明显的延迟,因为setTimeout在执行的时候首先会将任务放入到延迟消息队列里,等到showName执行完之后,才会去延迟队列里去查找已过期的任务,这里setTimeout任务会被showName耽误



  1. setTimeout嵌套下会有4ms的延迟


Chrome会把嵌套5层以上的setTimeout后当作阻塞方法,在第6次调用setTimeout的时候会自动将延时器更改为至少4ms的延迟时间



  1. 未激活的页面的setTimeout更改为至少1000ms


当前tab页面不在active状态的时候,setTimeout的延迟至少会被更改1000ms,这样做是为了减少性能消耗和电量消耗



  1. 延迟时间有最大值


目前Chrome、Firefox等主流浏览器都是用32bit去存储延时时间,所以最大值是2的31次方 - 1


  setTimeout(() => {
console.log(1)
}, 2 ** 31)

以上代码会立即执行


作者:我在曾经眺望彼岸
链接:https://juejin.cn/post/7032091028609990692

收起阅读 »

Android 图形处理 —— Matrix 原理剖析

Matrix 简介 Android 图形库中的 android.graphics.Matrix 是一个 3×3 的 float 矩阵,其主要作用是坐标变换 它的结构大概是这样的 其中每个位置的数值作用和其名称所代表的的含义是一一对应的 MSCALE_X、M...
继续阅读 »

Matrix 简介


Android 图形库中的 android.graphics.Matrix 是一个 3×3 的 float 矩阵,其主要作用是坐标变换


它的结构大概是这样的


matrix


其中每个位置的数值作用和其名称所代表的的含义是一一对应的



  • MSCALE_X、MSCALE_Y:控制缩放

  • MTRANS_X、MTRANS_Y:控制平移

  • MSKEW_X、MSKEW_X:控制错切

  • MSCALE_X、MSCALE_Y、MSKEW_X、MSKEW_X:控制旋转

  • MPERSP_0、MPERSP_1、MPERSP_2:控制透视


matrix_1


在 Android 中,我们直接实例化一个 Matrix,内部的矩阵长这样:


matrix_3


是一个左上到右下为 1,其余为 0 的矩阵,也叫单位矩阵,一般数学上表示为 I


Matrix 坐标变换原理


前面说到 Matirx 主要的作用就是处理坐标的变换,而坐标的基本变换有:平移、缩放、旋转和错切



这里所说的基本变换,也称仿射变换 ,透视不属于仿射变化,关于透视相关的内容不在本文的范围内



当矩阵的最后一行是 0,0,1 代表该矩阵是仿射矩阵,下文中所有的矩阵默认都是仿射矩阵


线性代数中的矩阵乘法


在正式介绍 Matrix 是如何控制坐标变换的原理之前,我们先简单复习一下线性代数中的矩阵乘法,详细的讲解可参见维基百科或者翻翻大学的《线性代数》,这里只做最简单的介绍




  • 两个矩阵相乘,前提是第一个矩阵的列数等于第二个矩阵的行数




  • 若 A 为 m × n 的矩阵,B 为 n × p 的矩阵,则他们的乘积 AB 会是一个 m × p 的矩阵,表达可以写为





  • 由定义计算,AB 中任意一点(a,b)的值为 A 中第 a 行的数和 B 中第 b 列的数的乘积的和







了解矩阵乘法的基本方法之后,我们还需要记住几个性质,对后续的分析有用



  • 满足结合律,即 A(BC)=(AB)C

  • 满足分配律,即 A(B + C) = AB + AC (A + B)C = AC + BC

  • 不满足交换律,即 AB != BA

  • 单位矩阵 I 与任意矩阵相乘,等于矩阵本身,即 IA = ABI = B


缩放(Scale)


我们先想想,让我们实现把一个点 (x0, y0) 的 x 轴和 y 轴分别缩放 k1 和 k2 倍,我们会怎么做,很简单


val x = k1 * x0
val y = k2 * y0

那如果用矩阵怎么实现呢,前面我们讲到 Matrix 中 MSCALE_XMSCALE_Y 是用来控制缩放的,我们在这里填分别设置为 k1 和 k2,看起来是这样的


image-20211109103257621

而点 (x0, y0) 用矩阵表示是这样的


image-20211109103824496

有些人会疑问,最后一行这里不是还有一个 1 吗,这是使用了齐次坐标系的缘故,在数学中我们的点和向量都是这样表示的 (x, y),两者看起来一样,计算机无法区分,为了让计算机也可以区分它们,增加了一个标志位,即


(x, y, 1) -> 点
(x, y, 0) -> 向量

现在 Matrix 和点都可以用矩阵表示了,接下来我们看看怎么通过这两个矩阵得到一个缩放之后的点 (x, y). 前面我们已经介绍过矩阵的乘法,让我们看看把上面两个矩阵相乘会得到什么结果


image-20211109104922576

可以看到,矩阵相乘得到了一个(k1x0, k2y0,1)的矩阵,上面说过,计算机中,这个矩阵就代表点 (k1x0, k2y0), 而这个点刚好就是我们要的缩放之后的点


以上所有过程用代码来实现,看起来就是像下面这样


val xy = FloatArray(x0, y0)
Matrix().apply {
setScale(k1, k2)
mapPoints(xy)
}

平移(Translate)


平移和缩放也是类似的,实现平移,我们一般可写为


val x = x0 + deltaX
val y = y0 + deltaY

而用矩阵来实现则是


val xy = FloatArray(x0, y0)
Matrix().apply {
setTranslate(k1, k2)
mapPoints(xy)
}

换成数学表示


translate


根据矩阵乘法


x = 1 × x0 + 0 × y0 + deltaX × 1 = x0 + deltaX
y = 0 × x0 + 1 × y0 + deltaY × 1 = y0 + deltaY

可得和一开始的实现也是效果一致的


错切(Skew)


错切相对于平移和缩放,可能大部分人对这个名词比较陌生,直接看三张图大家可能会比较直观


水平错切


x = x0 + ky0
y = y0

矩阵表示



水平错切


垂直错切


x = x0
y = kx0 + y0

矩阵表示




复合错切


x = x0 + k1y0
y = k2x0 + y0

矩阵表示




旋转(Rotate)


旋转相对以上三种变化又有一点复杂,这里涉及一些三角函数的计算,忘记的可以去维基百科 先复习下



image-20211108215739508

同样我们先自己实现一下旋转,假设一个点 A(x0, y0), 距离原点的距离为 r,与水平夹角为 α,现绕原点顺时针旋转 θ 度,旋转之后的点为 B(x, y)



用矩阵表示




Matrix 复合操作原理


前面介绍了四种基本变换,如果我们需要同时应用上多种变化,比如先绕原点顺时针旋转 90° 再 x 轴平移 100,y 轴平移 100, 最后 x、y 轴缩放0.5 倍,那么就需要用到复合操作


还是先用自己的实现来实现一下


x = ((x0 · cosθ - y0 · sinθ) + 100) · 0.5
y = ((y0 · cosθ + x0 · sinθ) + 100) · 0.5

矩阵表示


image-20211206155715836


按照前面的方式逐个推导,最终也能得到和上述一样的结果


到此,我们可以对 Matrix 做出一个基本的认识:Matrix 基于矩阵计算的原理,解决了计算机中坐标映射和变化的问题


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

Glide线程池

hello:大家好我是 小小小小小鹿,一枚菜鸡Android程序猿。最近正在阅读Glide源码,今天我们要研究的部分是Glide 线程池的配置。 本次代码阅读主要有两个目标 弄清楚Glide是如何做线程池配置的 Glide如何进行优先级加载 Glide用来...
继续阅读 »

hello:大家好我是 小小小小小鹿,一枚菜鸡Android程序猿。最近正在阅读Glide源码,今天我们要研究的部分是Glide 线程池的配置。 本次代码阅读主要有两个目标



  1. 弄清楚Glide是如何做线程池配置的

  2. Glide如何进行优先级加载


Glide用来进行图片加载,我们知道当页面暂停的时候,glide可以根据页面的生命周期,来暂停当前页面的请求,但是如果当前页面通过滑动加载大量图片,那么Glide是怎么进行图片加载的呢?是先调用的加载在前还是后调用的加载在前面呢?如果某个页面的部分图片需要优先被加载,那么Glide又该如何处理呢?


Glide线程池的使用


Glide DecodeJob 的工作过程我们知道Glide在进行一次完成的数据加载会经历 ResourceCacheGenerator --> DataCacheGenerator --> SourceGenerator 的三个过程变化。而在这个过程变化中会涉及到两个线程池的使用。




  1. EngineJob#start开始本次请求


    public synchronized void start(DecodeJob<R> decodeJob) {
     this.decodeJob = decodeJob;
       //如果是从 缓存中获取图片使用 diskCacheExecutor
     GlideExecutor executor = decodeJob.willDecodeFromCache()
         ? diskCacheExecutor
        : getActiveSourceExecutor();
     executor.execute(decodeJob);
    }

    private GlideExecutor getActiveSourceExecutor() {
        //如果useUnlimitedSourceGeneratorPool 为true 使用无限制的线程池
        //如果useAnimationPool 为true且如果useUnlimitedSourceGeneratorPool为false 使用动画线程池 否则使用sourceExecutor
       return useUnlimitedSourceGeneratorPool
           ? sourceUnlimitedExecutor : (useAnimationPool ? animationExecutor : sourceExecutor);
    }



  2. EngineJob#reschedule重新进行调度


    @Override
    public void reschedule(DecodeJob<?> job) {
     //此时线程池的使用逻辑和EngineJob#start不在文件中加载数据一致
     getActiveSourceExecutor().execute(job);
    }



Glide线程池的配置


Glide Excutor参数初始化来自于GlideBuilder#build 而这些在不额外设置的情况下都来自于GlideExecutor。而GlideExecutor的所有线程池都是通过配置ThreadPoolExecutor来完成的。


初识ThreadPoolExecutor


ExecutorService是最初的线程池接口,ThreadPoolExecutor类是对线程池的具体实现,它通过构造方法来配置线程池的参数。


public ThreadPoolExecutor(int corePoolSize,
                             int maximumPoolSize,
                             long keepAliveTime,
                             TimeUnit unit,
                             BlockingQueue<Runnable> workQueue,
                             ThreadFactory threadFactory,
                             RejectedExecutionHandler handler) {
       if (corePoolSize < 0 ||
           maximumPoolSize <= 0 ||
           maximumPoolSize < corePoolSize ||
           keepAliveTime < 0)
           throw new IllegalArgumentException();
       if (workQueue == null || threadFactory == null || handler == null)
           throw new NullPointerException();
       this.corePoolSize = corePoolSize;
       this.maximumPoolSize = maximumPoolSize;
       this.workQueue = workQueue;
       this.keepAliveTime = unit.toNanos(keepAliveTime);
       this.threadFactory = threadFactory;
       this.handler = handler;
  }

参数解释:


corePoolSize,线程池中核心线程的数量,默认情况下,即使核心线程没有任务在执行它也存在的,我们固定一定数量的核心线程且它一直存活这样就避免了一般情况下CPU创建和销毁线程带来的开销。我们如果将ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,那么闲置的核心线程就会有超时策略,这个时间由keepAliveTime来设定,即keepAliveTime时间内如果核心线程没有回应则该线程就会被终止。allowCoreThreadTimeOut默认为false,核心线程没有超时时间。 maximumPoolSize,线程池中的最大线程数,当任务数量超过最大线程数时其它任务可能就会被阻塞。最大线程数=核心线程+非核心线程。非核心线程只有当核心线程不够用且线程池有空余时才会被创建,执行完任务后非核心线程会被销毁。 keepAliveTime,非核心线程的超时时长,当闲置时间超过这个时间时,非核心线程就会被回收。当allowCoreThreadTimeOut设置为true时,此属性也作用在核心线程上。 unit,枚举时间单位,TimeUnit。 workQueue,线程池中的任务队列,我们提交给线程池的runnable会被存储在这个对象上。 线程池的分配遵循这样的规则:


当线程池中的核心线程数量未达到最大线程数时,启动一个核心线程去执行任务; 如果线程池中的核心线程数量达到最大线程数时,那么任务会被插入到任务队列中排队等待执行; 如果在上一步骤中任务队列已满但是线程池中线程数量未达到限定线程总数,那么启动一个非核心线程来处理任务; 如果上一步骤中线程数量达到了限定线程总量,那么线程池则拒绝执行该任务,且ThreadPoolExecutor会调用RejectedtionHandler的rejectedExecution方法来通知调用者。


threadFactory:线程工厂,为线程池提供创建新线程的能力。


DiskCacheExecutor的配置过程


GlideExecutor提供了三个创建DiskCacheExecutor的方法,最终都会调用到有三个参数那个


public static GlideExecutor newDiskCacheExecutor(
   int threadCount, String name, UncaughtThrowableStrategy uncaughtThrowableStrategy) {
 return new GlideExecutor(
     new ThreadPoolExecutor(
         threadCount /* corePoolSize */,
         threadCount /* maximumPoolSize */,
         0 /* keepAliveTime */,
         TimeUnit.MILLISECONDS,
         new PriorityBlockingQueue<Runnable>(),
         new DefaultThreadFactory(name, uncaughtThrowableStrategy, true)));
}

在默认创建的时候,调用的是无参数的那个,threadCount 值为1 即DiskCacheExecutor是一个核心线程数为1,没有非核心线程的线程池,所有任务在线程池中串行执行,Runnable的存储对象是PriorityBlockingQueue。


SourceExecutor的配置过程


public static GlideExecutor newSourceExecutor() {
 return newSourceExecutor(
     calculateBestThreadCount(),
     DEFAULT_SOURCE_EXECUTOR_NAME,
     UncaughtThrowableStrategy.DEFAULT);
}

public static GlideExecutor newSourceExecutor(
     int threadCount, String name, UncaughtThrowableStrategy uncaughtThrowableStrategy) {
   return new GlideExecutor(
       new ThreadPoolExecutor(
           threadCount /* corePoolSize */,
           threadCount /* maximumPoolSize */,
           0 /* keepAliveTime */,
           TimeUnit.MILLISECONDS,
           new PriorityBlockingQueue<Runnable>(),
           new DefaultThreadFactory(name, uncaughtThrowableStrategy, false)));
}

可以看到SourceExecutor的构建过程和基本一致,不同的地方在于核心线程的数量是通过calculateBestThreadCount来动态计算的。


if (bestThreadCount == 0) {
   //如果cpu核心数超过4则核心线程数为4 如果Cpu核心数小于4那么使用Cpu核心数作为核心线程数量
 bestThreadCount =
     Math.min(MAXIMUM_AUTOMATIC_THREAD_COUNT, RuntimeCompat.availableProcessors());
}
return bestThreadCount;

UnlimitedSourceExecutor无限制的线程池


public static GlideExecutor newUnlimitedSourceExecutor() {
 return new GlideExecutor(new ThreadPoolExecutor(
     0,
     Integer.MAX_VALUE,
     KEEP_ALIVE_TIME_MS,
     TimeUnit.MILLISECONDS,
     new SynchronousQueue<Runnable>(),
     new DefaultThreadFactory(
         SOURCE_UNLIMITED_EXECUTOR_NAME,
         UncaughtThrowableStrategy.DEFAULT,
         false)));
}

UnlimitedSourceExecutor没有核心线程,非核心线程数量无限大。


AnimationExecutor


public static GlideExecutor newAnimationExecutor() {
 int bestThreadCount = calculateBestThreadCount();
 int maximumPoolSize = bestThreadCount >= 4 ? 2 : 1;
 return newAnimationExecutor(maximumPoolSize, UncaughtThrowableStrategy.DEFAULT);
}

public static GlideExecutor newAnimationExecutor(
     int threadCount, UncaughtThrowableStrategy uncaughtThrowableStrategy) {
    return new GlideExecutor(
       new ThreadPoolExecutor(
           0 /* corePoolSize */,
           threadCount,
           KEEP_ALIVE_TIME_MS,
           TimeUnit.MILLISECONDS,
           new PriorityBlockingQueue<Runnable>(),
           new DefaultThreadFactory(
               ANIMATION_EXECUTOR_NAME,
               uncaughtThrowableStrategy,
               true)));
}

AnimationExecutor没有核心线程,非核心线程数量根据Cpu核心数来决定,当Cpu核心数大于等4时 非核心线程数为2,否则为1。


Glide线程池总结


DiskCacheExecutor和SourceExecutor 采用固定核心线程数固定,适用于处理CPU密集型的任务,但是没有非核心线程。确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。


UnlimitedSourceExecutor采用无核心线程,非核心线程无限大适用于并发执行大量短期的小任务。在空闲的时候消耗资源非常少。


AnimationExecutor没有核心线程,非核心线程有限,同UnlimitedSourceExecutor的区别就是核心线程数量和工作队列不一致。第一次看到这么用。


Glide如何实现加载优先级


除了UnlimitedSourceExecutor其余的都是使用的PriorityBlockingQueue。PriorityBlockingQueue是一个具有优先级的无界阻塞队列。也就是说优先级越高越先执行。


我们知道图片的加载是在线程池中执行的DecodeJob,DecodeJob实现了Runnable和Comparable接口。当DecodeJob被提交到线程池的时候,如果需要加入工作队列会通过compareTo比较Decodejob优先级


@Override
public int compareTo(@NonNull DecodeJob<?> other) {
 //先比较 Priority  
 int result = getPriority() - other.getPriority();
 //如果 Priority优先级一致 ,比较order order是一个自增的int 每一次初始化DecodeJob 都会执行++ 因此后初始化的DecodeJob比先初始化的优先级高。
 if (result == 0) {
   result = order - other.order;
}
 return result;
}
作者:小小小小小鹿
链接:https://juejin.cn/post/7038795986482757669
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Kotlin开发中的一些Tips

作用域函数选择 目前有let、run、with、apply 和 also五个作用域函数。 官方文档有张表来说明它们之间的区别:   总结一下有几点区别: 1、apply和also返回上下文对象。 2、let、run 和with返回lambda 结果。 3、l...
继续阅读 »

作用域函数选择


目前有letrunwithapply 和 also五个作用域函数。


官方文档有张表来说明它们之间的区别: 



 总结一下有几点区别:


1、applyalso返回上下文对象。


2、letrun 和with返回lambda 结果。


3、letalso引用对象是it ,其余是this


1.letrun是我日常使用最多的两个,它们之间很类似。


private var textView: TextView? = null

textView?.let {
it.text = "Kotlin"
it.textSize = 14f
}

textView?.run {
text = "Kotlin"
textSize = 14f
}

相比较来说使用run显得比较简洁,但let的优势在于可以将it重命名,提高代码的可读性,也可以避免作用域函数嵌套时导致混淆上下文对象的情况。


2.对于可空对象,使用let比较方便。对于非空对象可以使用with


3.applyalso也非常相似,文档给出的建议是如果是对象配置操作使用apply,额外的处理使用also。例如:


val numberList = mutableListOf()
numberList.also { println("Populating the list") }
.apply {
add(2.71)
add(3.14)
add(1.0)
}
.also { println("Sorting the list") }
.sort()

简单说就是符合单词的含义使用,提高代码可读性。


总的来说,这几种函数有许多重叠的部分,因此可以根据开发中的具体情况来使用。以上仅做参考。


Sequence


我们经常会使用到kotlin的集合操作符,比如 map 和 filter 等。


list.map {
it * 2
}.filter {
it % 3 == 0
}

老规矩,看一下反编译后的代码: 



就干了这么点事情,创建了两个集合,循环了两遍。效率太低,这还不如自己写个for循环,一个循环就处理完了。看一下map的源码:


public inline fun  Iterable.map(transform: (T) -> R): List {
return mapTo(ArrayList(collectionSizeOrDefault(10)), transform)
}

public inline fun > Iterable.mapTo(destination: C, transform: (T) -> R): C {
for (item in this)
destination.add(transform(item))
return destination
}

内部实现确实如此,难道这些操作符不香了?


其实这时就可以使用Sequences(序列),用法很简单,只需要在集合后添加一个asSeqence() 方法。


list.asSequence().map {
it * 2
}.filter {
it % 3 == 0
}

反编译:


SequencesKt.filter(SequencesKt.map(CollectionsKt.asSequence((Iterable)list), (Function1)null.INSTANCE), (Function1)null.INSTANCE);

有两个Function1,其实就是lambda表达式,这是因为Sequence没有使用内联导致的。我们先看看SequencesKt.map源码:


public fun  Sequence.map(transform: (T) -> R): Sequence {
return TransformingSequence(this, transform)
}

internal class TransformingSequence
constructor(private val sequence: Sequence, private val transformer: (T) -> R) : Sequence {
override fun iterator(): Iterator = object : Iterator {
val iterator = sequence.iterator()
override fun next(): R {
return transformer(iterator.next())
}

override fun hasNext(): Boolean {
return iterator.hasNext()
}
}

internal fun flatten(iterator: (R) -> Iterator): Sequence {
return FlatteningSequence(sequence, transformer, iterator)
}
}

可以看到没有创建中间集合去循环,只是创建了一个Sequence对象,里面实现了迭代器。SequencesKt.filter方法也是类似。细心的话你会发现,这都只是创建Sequence对象,所以要想真正拿到处理后的集合,需要添加toList()这种末端操作。


map 和 filter 这类属于中间操作,返回的是一个新Sequence,里面有数据迭代时的实际处理。而 toList和first这类属于末端操作用来返回结果。


所以Sequence是延迟执行的,这也就是它为何不会出现我们一开始提到的问题,一次循环就处理完成了。


总结一下Sequence的使用场景:


1、有多个集合操作符时,建议使用Sequence。


2、数据量大的时候,这样可以避免重复创建中间集合。这个数据量大,怎么也是万以上的级别了。


所以对于一般Android开发中来说,不使用Sequence其实差别不大。。。哈哈。。


协程


有些人会错误理解kotlin的协程,觉得它的性能更高,是一种“轻量级”的线程,类似go语言的协程。但是如果你细想一下,这是不太可能的,最终它都是要在JVM上运行,java都没有的东西,你就实现了,你这不是打java的脸嘛。


所以对于JVM平台,kotlin的协程只能是对Thread API的封装,和我们用的Executor类似。所以对于协程的性能,我个人也认为差别不大。只能说kotlin借助语言简洁的优势,让操作线程变的更加简单。


之所以上面说JVM,是因为kotlin还有js和native平台。对于它们来说,或许可以实现真正的协程。


推荐扔物线大佬关于协程的文章,帮你更好的理解kotlin的协程:到底什么是「非阻塞式」挂起?协程真的更轻量级吗?


Checked Exception


这对熟悉Java的同学并不陌生,Checked Exception 是处理异常的一种机制,如果你的方法中声明了它可能会抛出的异常,编译器就会强制开发者对异常进行处理,否则编译不会通过。我们需要使用 try catch 捕获异常或者使用 throws 抛出异常处理它。


但是Kotlin中并不支持这个机制,也就是说不会强制你去处理抛出的异常。至于Checked Exception 好不好,争议也不少。这里就不讨论各自的优缺点了。


既然Kotlin中没有这个机制已经是既成事实,那么我们在使用中就需要考虑它带来的影响。比如我们开发中在调用一些方法时,要注意看一下源码中是否有指定异常抛出,然后做相应处理,避免不必要的崩溃。


例如常用的json解析:


private fun test() {
val jsonObject = JSONObject("{...}")
jsonObject.getString("id")
...
}

在java中我们需要处理JSONException,kotlin中因为没有Checked Exception,如果我们像上面这样直接使用,虽然程序可以运行,可是一但解析出现异常,程序就会崩溃。


Intrinsics检查


如果你经常观察反编译后的java代码,会发现有许多类似Intrinsics.checkXXX这样的代码。


fun test(str: String) {
println(str)
}

反编译: 



 比如图中的checkParameterIsNotNull就是用了检查参数是否为空。虽然我们的参数是不可控的,但是考虑到方法会被Java调用,Kotlin会默认的增加checkParameterIsNotNull校验。如果kotlin方法是私有的,也就不会有此行检查。


checkParameterIsNotNull并不会有性能问题,相反这种提前判断参数是否正确,可以避免程序向后执行导致不必要的资源消耗。


当然如果你想去除它,可以添加下面的配置到你的gradle文件,这样就会在编译时去除它。


kotlinOptions {
freeCompilerArgs = [
'-Xno-param-assertions',
'-Xno-call-assertions',
'-Xno-receiver-assertions'
]
}

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

一图秒懂CDN原理

CDN
前些天,线上灰度了一个功能,下午接到一些业务上报国外用户访问时图片无法显示,但是国内访问都是正常,所以怀疑是国外CDN问题导致。 先了说明下现状: 图片保存在阿里OSS中 国内使用了阿里云CDN 国外使用Akamai(全球CDN厂商) 按理说,CDN都有...
继续阅读 »

前些天,线上灰度了一个功能,下午接到一些业务上报国外用户访问时图片无法显示,但是国内访问都是正常,所以怀疑是国外CDN问题导致。


先了说明下现状:



  1. 图片保存在阿里OSS中

  2. 国内使用了阿里云CDN

  3. 国外使用Akamai(全球CDN厂商)



按理说,CDN都有,图片不应该访问不到。于是,在脑子中根据CDN的原理,先思考下可能的问题



CDN原理


CDN全称是Content Delivery Network,即内容分发网络,也称为内容传送网络。CDN是构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。


cdn.jpg


如上图CDN的逻辑主要分为两步:DNS解析请求边缘节点



用dig看下DNS解析结果:



$ dig juejin.cn

; <<>> DiG 9.10.6 <<>> juejin.cn
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 63296
;; flags: qr rd ra; QUERY: 1, ANSWER: 9, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;juejin.cn. IN A

;; ANSWER SECTION:
juejin.cn. 412 IN CNAME juejin.cn.w.cdngslb.com.
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.229
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.227
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.231
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.224
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.225
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.230
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.226
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.228

;; Query time: 9 msec
;; SERVER: 192.168.3.1#53(192.168.3.1)
;; WHEN: Sat May 15 14:26:26 CST 2021
;; MSG SIZE rcvd: 203

在ANSWER SECTION列表可以看出



  1. juejin.cn为cname记录指向juejin.cn.w.cdngslb.com

  2. juejin.cn.w.cdngslb.com返回了7条A记录,这7个ip 信息是江苏 徐州 联通,我所在地是上海,联通,可以看出返回的都是就近节点。实际上CDN是有非常多的边缘节点。



用tcpdump来监控下DNS的UDP数据包




  1. 在一个窗口输入sudo tcpdump -n -s 1500 udp and port 53

  2. 在另一个窗口输入ping juejin.cn


监控到的UDP数据包如下:


21:49:13.960212 IP 192.168.3.201.52647 > 192.168.3.1.53: 37581+ A? juejin.cn. (27)
21:49:13.975290 IP 192.168.3.1.53 > 192.168.3.201.52647: 37581 9/0/0 CNAME juejin.cn.w.cdngslb.com., A 112.85.251.229, A 112.85.251.230, A 112.85.251.226, A 112.85.251.228, A 112.85.251.224, A 112.85.251.231, A 112.85.251.225, A 112.85.251.227 (192)

其中,192.168.3.1为路由器IP。也就是本机向路由器询问DNS解析,如果路由器已经缓存了,就会直接返回。


复现问题


我们回到问题中,如果CDN返回的边缘节点如果不出问题,图片应该是可以很快访问到的,CDN厂商不至于出现这个问题。那么问题在那里呢?


在公司环境无法复现问题,就要找一个最接近客户场景的环境来测试,于是想办法搞到一台香港window系统的测试机,远程上去一看,还果真有问题。



图片在界面中不显示,但是直接在浏览器访问是正常的,开发者模式下发现访问图片时出现跨域错误



一张正常显示的图片请求返回的http头是这样的:


Response Headers:
accept-ranges: bytes
access-control-allow-origin: *
etag: "A31F477F3232DA431D3B77543C3EBF92"
last-modified: Thu, 25 Jul 2019 01:37:03 GMT
...省略

1. access-control-allow-origin


通配符 * 表示允许被任何网站引用。如果想让资源只被指定域名访问,只需把*改为域名就行了,如下:


access-control-allow-origin: `https://juejin.cn`

2. etag


etag是http协议缓存逻辑中的一个属性。CDN的目的就是减少网络访问,因为缓存是必须要用的功能。


而无法显示的图片,返回的请求头是这样的:


Response Headers:
accept-ranges: bytes
etag: "A31F477F3232DA431D3B77543C3EBF92"
last-modified: Thu, 25 Jul 2019 01:37:03 GMT
...省略

没有access-control-allow-origin这一项,导致页面中无法加载。


浏览器边缘节点请求图片命中缓存,返回图片响应头中没有CORS属性抛出CORS异常,图片不渲染浏览器边缘节点


解决办法很简单,在CDN后台配置返回access-control-allow-origin信息即可


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

Flutter: 完成一个图片APP

自从 Flutter 推出之后, 一直是备受关注, 有看好的也有不看好的, 作为移动开发人员自然是要尝试一下的(但是它的嵌套写法真的难受), 本着学一个东西, 就一定要动手的态度, 平时又喜欢看一些猫狗的图片, 就想着做一个加载猫狗图片你的 APP, 界面图如...
继续阅读 »

自从 Flutter 推出之后, 一直是备受关注, 有看好的也有不看好的, 作为移动开发人员自然是要尝试一下的(但是它的嵌套写法真的难受), 本着学一个东西, 就一定要动手的态度, 平时又喜欢看一些猫狗的图片, 就想着做一个加载猫狗图片你的 APP, 界面图如下(界面不是很好看).






主要模块



NetWork

api.dart文件中, 分别定义了DogApi, CatApi两个类, 一个用于处理获取猫的图片的类, 一个用于处理狗的图片的类.


http_request.dart文件封装了Http请求, 用于发送和接收数据.


url.dart文件封装了需要用到的Api接口, 主要是为了方便和统一管理而编写.


Models文件夹下分别定义不同API接口返回数据的模型.


图片页

瀑布流使用的flutter_staggered_grid_view库, 作者自定义了Delegate计算布局, 使用起来非常简单.


Widget scene = new StaggeredGridView.countBuilder(
physics: BouncingScrollPhysics(),
itemCount: this.breedImgs != null ? this.breedImgs.urls.length : 0,
mainAxisSpacing: 4.0,
crossAxisSpacing: 4.0,
crossAxisCount: 3,
itemBuilder: (context, index) {
return new GestureDetector(
onTapUp: (TapUpDetails detail) {
// 展示该品种的相关信息
dynamic breed = this.breeds[this.selectedIdx].description;
// TODO: 取出当前点击的然后所有往后的
List<String> unreadImgs = new List<String>();
for (int i = index; i < this.breedImgs.urls.length; i++) {
unreadImgs.add(this.breedImgs.urls[i]);
}
AnimalImagesPage photoPage = new AnimalImagesPage(
listImages: unreadImgs,
breed: this.breeds[this.selectedIdx].name,
imgType: "Cat",
petInfo: this.breeds[this.selectedIdx],
);
Navigator.of(context)
.push(new MaterialPageRoute(builder: (context) {
return photoPage;
}));
},
child: new Container(
width: 100,
height: 100,
color: Color(0xFF2FC77D), //Colors.blueAccent,
child: new CachedNetworkImage(
imageUrl: this.breedImgs.urls[index],
fit: BoxFit.fill,
placeholder: (context, index) {
return new Center(child: new CupertinoActivityIndicator());
},
),
),
);
},
// 该属性可以控制当前 Cell 占用的空间大小, 用来实现瀑布的感觉
staggeredTileBuilder: (int index) =>
new StaggeredTile.count(1, index.isEven ? 1.5 : 1),
);

  • 组装PickerView


系统默认的 PickerView 在每一次切换都会回调, 而且没有确定和取消事件,
如果直接使用会造成频繁的网络请求, 内存消耗也太快, 所以组装了一下, 增加确定和取消才去执行网络请求, 这样就解决了这个问题.


    Widget column = Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
new Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
new Container(
width: MediaQuery.of(context).size.width,
height: 40,
child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
new Padding(
padding: EdgeInsets.only(left: 10.0),
child: new GestureDetector(
onTapUp: (detail) {
// 点击了确定按钮, 退出当前页面
Navigator.of(context).pop();
// 回调操作
this.submit(this.selectedIndex);
},
child: new Text(
"确定",
style: TextStyle(
decoration: TextDecoration.none,
color: Colors.white,
fontSize: 18),
),
),
),
new Padding(
padding: EdgeInsets.only(right: 10.0),
child: new GestureDetector(
onTapUp: (detail) {
// 点击了确定按钮, 退出当前页面
Navigator.of(context).pop();
},
child: new Text(
"取消",
style: TextStyle(
decoration: TextDecoration.none,
color: Colors.white,
fontSize: 18),
),
),
)
],
),
),
],
),
new Container(
height: 1,
color: Colors.white,
),
// Picker
new Expanded(
child: new CupertinoPicker.builder(
backgroundColor: Colors.transparent,
itemExtent: 44,
childCount: this.names.length,
onSelectedItemChanged: (int selected) {
this.selectedIndex = selected;
this.onSelected(selected);
},
itemBuilder: (context, index) {
return new Container(
width: 160,
height: 44,
alignment: Alignment.center,
child: new Text(
this.names[index],
textAlign: TextAlign.right,
style: new TextStyle(
color: Colors.white,
fontSize: 16,
decoration: TextDecoration.none),
),
);
}),
)
],
);
详情页


  • Column 包含 ListView


详情页中, 上方是一个图片, 下方是关于品种的相关信息, 下方是通过 API获取到的属性进行一个展示, 需要注意一点是, 如果Column封装了MainAxis相同方向的滚动控件, 必须设置Width/Height, 同理, Row也是需要注意这一点的.


我在这里的做法是通过一个Container包裹 ListView.


new Container(
margin: EdgeInsets.only(bottom: 10, top: 10),
height: MediaQuery.of(context).size.height - MediaQuery.of(context).size.width / 1.2 - 80,
width: MediaQuery.of(context).size.width,
child: listView,
),

  • 图片动画


这一部分稍微复杂一些, 首先需要监听滑动的距离, 来对图片进行变换, 最后根据是否达到阈值来进行切换动画, 这里我没有实现在最后一张和第一张图片进行切换以至于可以无限循环滚动, 我在边界阈值上只是阻止了下一步动画.


动画我都是通过Matrix4来设置不同位置的属性, 它也能模拟出 3D 效果,


动画的变换都是Tween来管理.


  void _initAnimation() {
// 透明度动画
this.opacityAnimation = new Tween(begin: 1.0, end: 0.0).animate(
new CurvedAnimation(
parent: this._nextAnimationController, curve: Curves.decelerate))
..addListener(() {
this.setState(() {
// 通知 Fluter Engine 重绘
});
});
// 翻转动画
// 第三个值是角度
var startTrans = Matrix4.identity()..setEntry(3, 2, 0.006);
var endTrans = Matrix4.identity()
..setEntry(3, 2, 0.006)
..rotateX(3.1415927);
this.transformAnimation = new Tween(begin: startTrans, end: endTrans)
.animate(new CurvedAnimation(
parent: this._nextAnimationController, curve: Curves.easeIn))
..addListener(() {
this.setState(() {});
});
// 缩放
var saveStartTrans = Matrix4.identity()..setEntry(3, 2, 0.006);
// 平移且缩放
var saveEndTrans = Matrix4.identity()
..setEntry(3, 2, 0.006)
..scale(0.1, 0.1)
..translate(-20.0, 20.0); // MediaQuery.of(context).size.height
this.saveToPhotos = new Tween(begin: saveStartTrans, end: saveEndTrans)
.animate(new CurvedAnimation(
parent: this._saveAnimationController, curve: Curves.easeIn))
..addListener(() {
this.setState(() {});
});
}

Widget引用这个属性来执行动画.


Widget pet = new GestureDetector(
onVerticalDragUpdate: nextUpdate,
onVerticalDragStart: nextStart,
onVerticalDragEnd: next,
child: new Transform(
transform: this.dragUpdateTransform,
child: Container(
child: new Transform(
alignment: Alignment.bottomLeft,
transform: transform,
child: new Opacity(
opacity: opacity,
child: Container(
width: MediaQuery.of(context).size.width / 1.2,
height: MediaQuery.of(context).size.width / 1.5 - 30,
child: new Padding(
padding: EdgeInsets.all(0),
child: new CachedNetworkImage(
imageUrl: this.widget.listImages[item],
fit: BoxFit.fill,
placeholder: (context, content) {
return new Container(
width: MediaQuery.of(context).size.width / 2.0 - 40,
height: MediaQuery.of(context).size.width / 2.0 - 60,
color: Color(0xFF2FC77D),
child: new Center(
child: new CupertinoActivityIndicator(),
),
);
},
),
),
),
),
),
),
),
);
Firebase_admob

注意: 这里需要去 firebase 官网注册 APP, 然后分别下载 iOS, Android 的配置文件放到指定的位置, 否则程序启动的时候会闪退.


iOS info.plist: GADApplicationIdentifier也需要配置, 虽然在 Dart 中会启动的时候就注册ID, 但是这里也别忘了配置.


Android Manifst.xml 也需要配置


<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value=""/>

这里说一下我因为个人编码导致的问题, 我尝试自己来控制广告展示, 加了一个读秒跳过按钮(想强制观看一段时间), 点击跳过设置setState, 但是在 build 方法中又请求了广告, 导致了一个死循环, 最后由于请求次数过多还没有设置自己的设备为测试设备也不是使用的测试ID, 账号被暂停了, 所以大家使用的时候要避免这个问题, 尽量还是将自己的设备添加到测试设备中.


使用的话比较简单(官方的演示代码直接复制也可以用).


class AdPage {
MobileAdTargetingInfo targetingInfo;

InterstitialAd interstitial;

BannerAd banner;

void initAttributes() {
if (this.targetingInfo == null) {
this.targetingInfo = MobileAdTargetingInfo(
keywords: ["some keyword for your app"],
// 防止被Google 认为是无效点击和展示.
testDevices: ["Your Phone", "Simulator"]);

bool android = Platform.isAndroid;

this.interstitial = InterstitialAd(
adUnitId: InterstitialAd.testAdUnitId,
targetingInfo: this.targetingInfo,
listener: (MobileAdEvent event) {
if (event == MobileAdEvent.closed) {
// 点击关闭
print("InterstitialAd Closed");
this.interstitial.dispose();
this.interstitial = null;
} else if (event == MobileAdEvent.clicked) {
// 关闭
print("InterstitialAd Clicked");
this.interstitial.dispose();
this.interstitial = null;
} else if (event == MobileAdEvent.loaded) {
// 加载
print("InterstitialAd Loaded");
}
print("InterstitialAd event is $event");
},
);

// this.banner = BannerAd(
// targetingInfo: this.targetingInfo,
// size: AdSize.smartBanner,
// listener: (MobileAdEvent event) {
// if (event == MobileAdEvent.closed) {
// // 点击关闭
// print("InterstitialAd Closed");
// this.interstitial.dispose();
// this.interstitial = null;
// } else if (event == MobileAdEvent.clicked) {
// // 关闭
// print("InterstitialAd Clicked");
// this.interstitial.dispose();
// this.interstitial = null;
// } else if (event == MobileAdEvent.loaded) {
// // 加载
// print("InterstitialAd Loaded");
// }
// print("InterstitialAd event is $event");
// });
}
}

@override
void show() {
// 初始化数据
this.initAttributes();
// 然后控制跳转
if (this.interstitial != null) {
this.interstitial.load();
this.interstitial.show(
anchorType: AnchorType.bottom,
anchorOffset: 0.0,
);
}
}
}

项目比较简单, 但是编写的过程中也遇到了许多问题, 慢慢解决的过程也学到了挺多.


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

Flutter页面传值的几种方式

今天来聊聊Flutter页面传值的几种方式: InheritWidget Notification Eventbus (当前Flutter版本:2.0.4) InheritWidget 如果看过Provider的源码的同学都知道,Provider跨组件传值...
继续阅读 »

今天来聊聊Flutter页面传值的几种方式:



  1. InheritWidget

  2. Notification

  3. Eventbus


(当前Flutter版本:2.0.4)


InheritWidget


如果看过Provider的源码的同学都知道,Provider跨组件传值的原理就是根据系统提供的InheritWidget实现的,让我们来看一下这个组件。
InheritWidget是一个抽象类,我们写一个保存用户信息的类UserInfoInheritWidget继承于InheritWidget:


class UserInfoInheritWidget extends InheritedWidget {

UserInfoBean userInfoBean;
UserInfoInheritWidget({Key key, this.userInfoBean, Widget child}) : super (child: child);

static UserInfoWidget of(BuildContext context){
return context.dependOnInheritedWidgetOfExactType<UserInfoWidget>();
}

@override
bool updateShouldNotify(UserInfoInheritWidget oldWidget) {
return oldWidget.userInfoBean != userInfoBean;
}
}

我们在这里面定义了一个静态方法:of,并且传入了一个context,根据context获取当前类,拿到当前类中的UserInfoBean,其实获取主题数据也是根据InheritWidget这种方式获取Theme.of(context),关于of方法后面重点讲一下,updateShouldNotify是刷新机制,什么时候刷新数据


还有一个用户信息的实体:


class UserInfoBean {
String name;
String address;
UserInfoBean({this.name, this.address});
}

我们做两个页面,第一个页面显示用户信息,还有一个按钮,点击按钮跳转到第二个页面,同样也是显示用户信息:


class Page19PassByValue extends StatefulWidget {
@override
_Page19PassByValueState createState() => _Page19PassByValueState();
}

class _Page19PassByValueState extends State<Page19PassByValue> {

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('PassByValue'),
),
body: DefaultTextStyle(
style: TextStyle(fontSize: 30, color: Colors.black),
child: Column(
children: [
Text(UserInfoWidget.of(context)!.userInfoBean.name),
Text(UserInfoWidget.of(context)!.userInfoBean.address),
SizedBox(height: 40),
TextButton(
child: Text('点击跳转'),
onPressed: (){
Navigator.of(context).push(CupertinoPageRoute(builder: (context){
return DetailPage();
}));
},
)
],
),
),
);
}
}

class DetailPage extends StatefulWidget {
@override
_DetailPageState createState() => _DetailPageState();
}

class _DetailPageState extends State<DetailPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Detail'),
),
body: DefaultTextStyle(
style: TextStyle(fontSize: 30, color: Colors.black),
child: Center(
child: Column(
children: [
Text(UserInfoWidget.of(context).userInfoBean.name),
Text(UserInfoWidget.of(context).userInfoBean.address),
TextButton(
onPressed: () {
setState(() {
UserInfoWidget.of(context)!.updateBean('wf123','address123');
});
},
child: Text('点击修改'))
],
),
),
)
);
}
}

由于我们这里是跨组件传值,需要把UserInfoWidget放在MaterialApp的上层,并给UserInfoBean一个初始值:


class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return UserInfoWidget(
userInfoBean: UserInfoBean(name: 'wf', address: 'address'),
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
),
);
}
}

这样就实现了一个跨组件传值,但是还有个问题,我们给UserInfoWidget赋值的时候是在最顶层,在真实业务场景中,如果我们把UserInfo的赋值放在MaterialApp上面,这时候我们还没拿到用户数据呢,所以就要有一个可以更新UserInfo的方法,并且修改后立即刷新,我们可以借助setState,把我们上面定义的UserInfoWidget改个名字然后封装在StatefulWidget 中:


class _UserInfoInheritWidget extends InheritedWidget {

UserInfoBean userInfoBean;
Function update;
_UserInfoInheritWidget({Key key, this.userInfoBean, this.update, Widget child}) : super (child: child);

updateBean(String name, String address){
update(name, address);
}

@override
bool updateShouldNotify(_UserInfoInheritWidget oldWidget) {
return oldWidget.userInfoBean != userInfoBean;
}
}

class UserInfoWidget extends StatefulWidget {
UserInfoBean userInfoBean;
Widget child;
UserInfoWidget({Key key, this.userInfoBean, this.child}) : super (key: key);

static _UserInfoInheritWidget of(BuildContext context){
return context.dependOnInheritedWidgetOfExactType<_UserInfoInheritWidget>();
}
@override
State<StatefulWidget> createState() => _UserInfoState();
}

class _UserInfoState extends State <UserInfoWidget> {

_update(String name, String address){
UserInfoBean bean = UserInfoBean(name: name, address: address);
widget.userInfoBean = bean;
setState(() {});
}
@override
Widget build(BuildContext context) {
return _UserInfoInheritWidget(
child: widget.child,
userInfoBean: widget.userInfoBean,
update: _update,
);
}
}

上面把继承自InheritWidget的类改了一个名字:_UserInfoInheritWidget,对外只暴露用StatefulWidget封装过的UserInfoWidget,向_UserInfoInheritWidget传入了包含setState的更新数据方法,更新数据的时候通过UserInfoWidget.of(context)获取到继承于InheritWidget_UserInfoInheritWidget类,调用updateBean方法实际上就调用了包含setState的方法,所以做到了数据更新和页面刷新


1.gif


下面重点说一下UserInfoWidget.of(context)是如何获取到继承于InheritWidget类的对象的,通过查看类似的方法:Theme.of(context)发现是根据dependOnInheritedWidgetOfExactType,于是我们也照着它的样子获取到了_UserInfoInheritWidget,点到dependOnInheritedWidgetOfExactType源码中看一下,发现跳转到了BuildContext中定义了这个方法:


  T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({ Object? aspect });
复制代码

了解WidgetElementRenderObject三只之间关系的同学都知道,其实contextElement的一个实例,BuildContext的注释也提到了这一点:


image.png
我们可以在Element中找到这个方法的实现:


@override
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
if (ancestor != null) {
assert(ancestor is InheritedElement);
return dependOnInheritedElement(ancestor, aspect: aspect) as T;
}
_hadUnsatisfiedDependencies = true;
return null;
}

_inheritedWidgets是从哪来的,我们搜索一下在Element中发现


void _updateInheritance() {
assert(_lifecycleState == _ElementLifecycle.active);
_inheritedWidgets = _parent?._inheritedWidgets;
}

再看一下_updateInheritance方法是什么时候调用的


@mustCallSuper
void mount(Element? parent, dynamic newSlot) {
...
...省略无关代码
_parent = parent;
_slot = newSlot;
_lifecycleState = _ElementLifecycle.active;
_depth = _parent != null ? _parent!.depth + 1 : 1;
if (parent != null) // Only assign ownership if the parent is non-null
_owner = parent.owner;
final Key? key = widget.key;
if (key is GlobalKey) {
key._register(this);
}
_updateInheritance();//这里调用了一次
}

还有:


@mustCallSuper
void activate() {
...
...已省略无关代码
final bool hadDependencies = (_dependencies != null && _dependencies!.isNotEmpty) || _hadUnsatisfiedDependencies;
_lifecycleState = _ElementLifecycle.active;
_dependencies?.clear();
_hadUnsatisfiedDependencies = false;
_updateInheritance();//这里又调用了一次
if (_dirty)
owner!.scheduleBuildFor(this);
if (hadDependencies)
didChangeDependencies();
}

从上面代码我们可以看到每个页面的Element都会通过_parent向下级传递父级信息,而我们的UserInfoWidget就保存在_parent中的_inheritedWidgets集合中:
Map<Type, InheritedElement>? _inheritedWidgets;,当_inheritedWidgets在页面树中向下传递的时候,如果当前WidgetInheritWidget,在当前Widget对应的Element中先看_parent传过来的_inheritedWidgets是否为空,如果为空就新建一个集合,把自己存到这个集合中,以当前的类型作为key(这也是为什么调用of方法中的context.dependOnInheritedWidgetOfExactType方法为什么要传当前类型的原因),从_inheritedWidgets集合中去取值;如果不为空直接把自己存进去,这就是of的原理了。


Notification


上面讲的InheritWidget一般是根部组建向子级组件传值,Notification是从子级组件向父级组件传值,下面我们来看一下它的用法


class Page19PassByValue extends StatefulWidget {
@override
_Page19PassByValueState createState() => _Page19PassByValueState();
}

class _Page19PassByValueState extends State<Page19PassByValue> {
UserInfoBean userInfoBean = UserInfoBean(name: 'wf', address: 'address');

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('PassByValue'),
),
body: Center(
child: NotificationListener<MyNotification>(
onNotification: (MyNotification data) {
userInfoBean = data.userInfoBean;
setState(() {});
///这里需要返回一个bool值,true表示阻止事件继续向上传递,false表示事件可以继续向上传递到父级组件
return true;
},
child: Builder(
///这里用了一个Builder包装了一下,为的是能取到
///NotificationListener的context
builder: (context) {
return Column(
children: [
Text(userInfoBean.name),
Text(userInfoBean.address),
Container(
child: FlatButton(
child: Text('点击传值'),
onPressed: () {
MyNotification(userInfoBean: UserInfoBean(name: 'wf123', address: 'address123')).dispatch(context);
},
),
)
],
);
},
),
),
),
);
}
}

///Notification是一个抽象类,
///使用Notification需要自定义一个class继承Notification
class MyNotification extends Notification {
UserInfoBean userInfoBean;
MyNotification({this.userInfoBean}) : super();
}

我们到源码中看一下这个dispatch方法:


void dispatch(BuildContext target) {
// The `target` may be null if the subtree the notification is supposed to be
// dispatched in is in the process of being disposed.
target?.visitAncestorElements(visitAncestor);
}

target就是我们传进来的context,也就是调用了BuildContextvisitAncestorElements方法,并且把visitAncestor方法作为一个参数传过去,visitAncestor方法返回一个bool值:


  @protected
@mustCallSuper
bool visitAncestor(Element element) {
if (element is StatelessElement) {
final StatelessWidget widget = element.widget;
if (widget is NotificationListener<Notification>) {
if (widget._dispatch(this, element)) // that function checks the type dynamically
return false;
}
}
return true;
}

我们进入Element内部看一下visitAncestorElements方法的实现:


@override
void visitAncestorElements(bool visitor(Element element)) {
assert(_debugCheckStateIsActiveForAncestorLookup());
Element? ancestor = _parent;
while (ancestor != null && visitor(ancestor))
ancestor = ancestor._parent;
}

当有父级节点,并且visitor方法返回true的时候执行while循环,visitorNotification类传进来的方法,回过头再看visitor方法的实现,当Elementvisitor方法传递的ancestorNotificationListener类的情况下,再判断widget._dispatch方法,而widget._dispatch方法:


final NotificationListenerCallback<T>? onNotification;

bool _dispatch(Notification notification, Element element) {
if (onNotification != null && notification is T) {
final bool result = onNotification!(notification);
return result == true; // so that null and false have the same effect
}
return false;
}

就是我们在外面写的onNotification方法的实现,我们在外面实现的onNotification方法返回true(即阻止事件继续向上传递),上面的while循环主要是为了执行我们onNotification里面的方法.


总结一下:MyNotification执行dispatch方法,传递context,根据当前context向父级查找对应NotificationListener,并且执行NotificationListener里面的onNotification方法,返回true,则事件不再向上级传递,如果返回false则事件继续向上一个NotificationListener传递,并执行里面对应的方法。Notification主要用在同一个页面中,子级向父级传值,比较轻量级,不过如果我们用了Provider可能就就直接借助Provider传值了。


Eventbus


Eventbus用于两个不同的页面,可以跨多级页面传值,用法也比较简单,我创建了一个EventBusUtil来创建一个单例


import 'package:event_bus/event_bus.dart';
class EventBusUtil {
static EventBus ? _instance;
static EventBus getInstance(){
if (_instance == null) {
_instance = EventBus();
}
return _instance!;
}
}

在第一个页面监听:


class Page19PassByValue extends StatefulWidget {
@override
_Page19PassByValueState createState() => _Page19PassByValueState();
}

class _Page19PassByValueState extends State<Page19PassByValue> {
UserInfoBean userInfoBean = UserInfoBean(name: 'wf', address: 'address');
@override
void initState() {
super.initState();
EventBusUtil.getInstance().on<UserInfoBean>().listen((event) {
setState(() {
userInfoBean = event;
});
});
}

@override
void dispose() {
super.dispose();
//不用的时候记得关闭
EventBusUtil.getInstance().destroy();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('PassByValue'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(userInfoBean.name),
Text(userInfoBean.address),
TextButton(onPressed: (){
Navigator.of(context).push(CupertinoPageRoute(builder: (_){
return EventBusDetailPage();
}));
}, child: Text('点击跳转'))

],
),
),
);
}
}

在第二个页面发送事件:


class EventBusDetailPage extends StatefulWidget {
@override
_EventBusDetailPageState createState() => _EventBusDetailPageState();
}

class _EventBusDetailPageState extends State<EventBusDetailPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('EventBusDetail'),
),
body: Center(
child: TextButton(onPressed: (){
EventBusUtil.getInstance().fire(UserInfoBean(name: 'name EventBus', address: 'address EventBus'));
}, child: Text('点击传值')),
),
);
}
}

我们看一下EventBus的源码,发现只有几十行代码,他的内部是创建了一个StreamController,通过StreamController来实现跨组件传值,我们也可以直接使用一下这个StreamController实现页面传值:


class Page19PassByValue extends StatefulWidget {
@override
_Page19PassByValueState createState() => _Page19PassByValueState();
}

StreamController controller = StreamController();

class _Page19PassByValueState extends State<Page19PassByValue> {

//设置一个初始值
UserInfoBean userInfoBean = UserInfoBean(name: 'wf', address: 'address');
@override
void initState() {
super.initState();
controller.stream.listen((event) {
setState(() {
userInfoBean = event;
});
});
}

@override
void dispose() {
super.dispose();
//页面销毁的时候记得关闭
controller.close();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('PassByValue'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(userInfoBean.name),
Text(userInfoBean.address),
TextButton(onPressed: (){
Navigator.of(context).push(CupertinoPageRoute(builder: (_){
return MyStreamControllerDetail();
}));
}, child: Text('点击跳转'))
],
),
)
);
}
}

class MyStreamControllerDetail extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _MyStreamControllerDetailState();
}
}
class _MyStreamControllerDetailState extends State <MyStreamControllerDetail> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('StreamController'),
),
body: Center(
child: TextButton(onPressed: (){
//返回上个页面,会发现页面的数据已经变了
controller.sink.add(UserInfoBean(name: 'StreamController pass name: 123', address: 'StreamController pass address 123'));
}, child: Text('点击传值'),),
),
);
}
}

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

Swift 指针的应用

iOS
Swift与指针由于Swift本身是一门较为现代的语言,支持很多高级特性,所以对于程序员来说,大部分时候不需要用到指针这种更“底层”的特性。而Swift语言的设计者也在尽可能希望开发者能尽量少的使用指针。但是,“慎用”不代表“不能用”,更不代表“没用”。相反,...
继续阅读 »

Swift与指针

由于Swift本身是一门较为现代的语言,支持很多高级特性,所以对于程序员来说,大部分时候不需要用到指针这种更“底层”的特性。而Swift语言的设计者也在尽可能希望开发者能尽量少的使用指针。

但是,“慎用”不代表“不能用”,更不代表“没用”。相反,指针非常有用,在某些场景下还是必不可少的特性。尤其是开发工作和系统底层特性、内存处理、高性能需求息息相关时。

所以,Swift通过在施加某种限制的前提下为开发者暴露了指针的使用接口,本篇文章重点介绍Swift使用指针的相关类型、函数,以及在实践应用中灵活使用指针解决问题的技巧。

类型限定的指针 UnsafePointer

Swift通过UnsafePointer<T>来指向一个类型为T的指针,该指针的内容是 只读 的,对于一个UnsafePointer<T>变量来说,通过pointee成员即可获得T的值。

func call(_ p: UnsafePointer<Int>) {
print("\(p.pointee)")
}
var a = 1234
call(&a) // 打印:1234

以上例子中函数call接收一个UnsafePointer<Int>类型作为参数,变量a通过在变量名前面加上&将其地址传给call。函数call直接打印指针的pointee成员,该成员就是a的值,所以最终打印结果为1234

注1:&aswift提供的语法特性,用于传递指针,但它有严格的适用场景限制。

注2:注意示例中对于变量a使用了var声明,而事实上UnsafePointer是“常量指针”,并不会修改a的内容,即使是这样a还是必须用var声明,如果用let会报错Cannot pass immutable value as inout argument: 'a' is a 'let' constant。这是因为swift规定UnsafePointer作为参数只能接收inout修饰的类型,而inout修饰的类型必然是可写的,所以使用var在所难免。

内容可写的类型限定指针 UnsafeMutablePointer

既然有 内容只读 指针,必须也得有 内容可读写 指针搭配才行,在Swift中,内容可读写的类型限定指针为UnsafeMutablePointer<T>类型,就和名字描述的那样,它和UnsafePointer最大的区别就是它指向的内容是可更改的,并且更改后指向的“数据源”也会被改动。

func modify(_ p: UnsafeMutablePointer<Int>) {
p.pointee = 5678
}
var a = 1234
modify(&a)
print("\(a)") // 打印:5678

在以上的例子中,指针p指向的值被重新赋值为5678,这也使得指针的“源”,即变量a的值发生变化,最终打印a的结果可以看出a被修改为5678

指针的辅助函数 withUnsafePointer

通过函数withUnsafePointer,获得指定类型的对应指针。该函数原型如下:

func withUnsafePointer<T, Result>(to value: inout T, _ body: (UnsafePointer<T>) throws -> Result) rethrows -> Result

这个函数原型看似很复杂很长,其实只要理解它所需要的信息只有两个:

  1. 指针指向类型是什么。
  2. 想要返回的指针地址是什么。
var a = 1234
let p = withUnsafePointer(to: &a) { $0 }
print("\(p.pointee)") // 打印:1234

以上例子是withUnsafePointer最精简的调用例子,我们定义了一个整形a,而p就是指向a的整形指针,事实上它的类型会被自动转换为UnsafePointer<Int>,第二个参数被简化为了{ $0 },它传入了一个代码块,代码块接收一个UnsafePointer<Int>参数,该参数即是a的地址,直接通过$0将它返回,即得到了a的指针,最终它被传给了p

对于第二个参数,或许有人会产生疑问,它似乎是没有意义的参数,大部分时候我们不是直接返回a的地址吗,为什么要多此一举通过代码块返回一次?这个疑问是合理的,绝大多数时候确实第二个参数显得有些多余,当然了,有时候可以通过第二个参数提供指针偏移的灵活性,如下例子可以提供一个案例。

var a = [1234, 5678]
let p = withUnsafePointer(to: &a[0]) { $0 + 1 }
print("\(p.pointee)") // 打印:5678

以上例子中,通过在第二个参数中对地址施加偏移,可以原来指向数组首个元素的地址偏移到第二个地址中。

另外,由于withUnsafePointer带着两个泛型参数,这意味着第二个参数可以是不同的类型。

var a = 1234
let p = withUnsafePointer(to: &a) { $0.debugDescription }
print("\(p)")

以上例子中,withUnsafePointer返回的并不是UnsafePointer<Int>类型,甚至不是指针,而是一个字符串,字符串保存着a对应指针的debug信息。

注1:同样的,和withUnsafePointer相对应的,还有withUnsafeMutablePointer,一样是只读和可读写的区别。读者可以自行测试用法。 注2:基本上Swift指针操作的with系列函数都提供了第二个参数用来灵活的提供函数的返回类型。

获取指针并进行字节级操作 withUnsafeBytes

有时候,我们需要对某块内存进行字节级编程。比如我们用一个32位整形来表示一个32位的内存块,对内存中的每个字节进行读写操作。

通过withUnsafeBytes,可以得到某个类型的数据的字节指针,从而可以对它们进行字节级编程。

var a: UInt32 = 0x12345678
let p = withUnsafeBytes(of: &a) { $0 }
var log = ""
for item in p {
let hex = NSString(format: "%x", item)
log += "\(hex)"
}
print("\(p.count)") // 打印:4
print("\(log)") // 对于小端机器会打印:78563412

在以上例子中,withUnsafeBytes返回了一个类型UnsafeRawBufferPointer,该类型代表着一个字节级的内存块,并提供了等价于数组操作,所以你可以通过下标索引、for循环的方式来处理返回的对象。

例子中的a是一个32位整形,所以p指针的count返回的是4,单位为字节。 在本例中,对内存块p从低到高逐字节的打印每个字节的16进制值。 具体打印出来的结果因运行的机器而异,在大端机器上,打印的结果是12345678,而在小端机器上打印结果则是78563412

注:大端和小端决定了一个基础数据单元在内存中是如何按序存放的,例如小端机器会将基本数据单元的低位放在内存的低位,由低到高排列,而大端机器则相反。具体相关知识可查阅维基百科。大部分情况下,同一台机器采用的字节序列是一致的,某些CPU可以配置大小端的切换。

指向连续内存的指针 UnsafeBufferPointer

Swift的数组提供了函数withUnsafeBufferPointer,通过它我们可以方便的用指针来处理数组。如下例子:

let a: [Int32] = [1, 2, -1, -2, 5, 6]
let p = a.withUnsafeBufferPointer { $0 }
print("\(p.count)") // 打印:6
print("\(p[3])") // 打印:-2

在该例子中,通过withUnsafeBufferPointer,可以获得变量pp的类型为UnsafeBufferPointer<Int32>,它代表着一整块的连续内存,我们可以像看待数组一样看待它,并且它也支持大部分数组操作。

指针的类型转换

介绍了那么多Swift中的指针类型,每一种都有各自的用途,但是在实际开发中,很可能我们需要将一个指针类型转换为特定的指针类型。

以下例子提供了几个类型指针之间的转换

let a: [Int32] = [1, 2, -1, -2, 5, 6]
// 类型 p: UnsafeBufferPointer<Int32>
let p = a.withUnsafeBufferPointer { $0 }
// 类型 p2: UnsafePointer<UInt32>
let p2 = p.baseAddress!.withMemoryRebound(to: UInt32.self, capacity: p.count) { $0 }
// 类型 p3: UnsafeBufferPointer<UInt32>
let p3 = UnsafeBufferPointer(start: p2, count: p.count)
print("\(p3.count)") // 打印:6
print("\(p3[3])") // 打印:4294967294

以上例子中,我们获得了以下三个指针类型

  1. UnsafeBufferPointer<Int32>类型的指针p
  2. UnsafePointer<UInt32>类型的指针p2
  3. UnsafeBufferPointer<UInt32>类型的指针p3

该例子有部分细节必须讲明,首先是baseAddress,通过该成员得到UnsafeBufferPointer基地址,获得的数据类型是UnsafePointer<>

由于a指向的元数据类型是Int32,所以其baseAddress类型即是UnsafePointer<Int32>

在本例中,我们将元数据类型由Int32改为UInt32,这里用到了UnsafePointer的成员函数withMemoryRebound,通过它将UnsafePointer<Int32>转换为UnsafePointer<UInt32>

最后一部分,我们创建了一个新的指针UnsafeBufferPointer,通过其构造函数,我们让该指针的起始位置设定为p2,元素个数设定为p的元素个数,这样就成功得到了一个UnsafeBufferPointer<UInt32>类型。

接下来的打印语句,我们可以看到p3类型的count成员依然是6,而p3[3]打印的结果却是4294967294,而不是数组a对应元素的-2,这是因为从p3的角度来看,它是用UInt32类型来“看待”原先的Int32数据元素。

回调函数的实用性

前面讨论withUnsafePointer时我曾经提过第二个回调参数似乎略显鸡肋,事实上它非常有用,通过回调函数,我们可以对上一段代码进行“优化”。

let a: [Int32] = [1, 2, -1, -2, 5, 6]
// 类型 p: UnsafeBufferPointer<Int32>
let p = a.withUnsafeBufferPointer { $0 }
// 类型 p3: UnsafeBufferPointer<UInt32>
let p3 = p.baseAddress!.withMemoryRebound(to: UInt32.self, capacity: p.count) {
UnsafeBufferPointer(start: $0, count: p.count)
}
print("\(p3.count)") // 打印:6
print("\(p3[3])") // 打印:4294967294

可以看到利用回调函数,我们把原先的p2p3代码合并了,这样可以让withMemoryRebound立刻返回UnsafeBufferPointer<UInt32>类型。

注:事实上该回调还可以不断“套娃”,也就是说可以直接把p3部分的代码和p也进行合并,但是出于可读性考虑,开发者应自己根据需要选择性进行嵌套。

Swift中的空指针:UnsafeRawPointer

就像C语言有void*(即空指针)一样,Swift也有自己的空指针,它通过类型UnsafeRawPointer来获得,我们知道,空指针没有指向特定的类型,又“可以”指向任何类型,灵活性极高,也需要程序员自己能够理解和处理好对应的细节。

同样是将UnsafeBufferPointer<Int32>转换为UnsafeBufferPointer<UInt32>,以下代码通过UnsafeRawPointer来实现。

let a: [Int32] = [1, 2, -1, -2, 5, 6]
let p = a.withUnsafeBufferPointer { $0 }
let p2 = UnsafeRawPointer(p.baseAddress!).assumingMemoryBound(to: UInt32.self)
let p3 = UnsafeBufferPointer(start: p2, count: p.count)
print("\(p3.count)") // 打印:6
print("\(p3[3])") // 打印:4294967294

在该例子中我们通过空指针完成了如下操作:

  1. UnsafeRawPointer通过构造函数接收了p的“基地址”构造了一个空指针类型。
  2. 由于构造的是空指针类型,我们需要对它进行类型转换,通过assumingMemoryBound把它转换成新的数据类型UnsafePointer<UInt32>
  3. 通过UnsafeBufferPointer构造函数重新构造了一个新的指针UnsafeBufferPointer<UInt32>

通过指针动态创建、销毁内存

有时候我们需要动态开辟和管理一块内存,最后释放它,Swift提供了UnsafeMutablePointer的成员函数allocate来处理该工作。

let p = UnsafeMutablePointer<Int32>.allocate(capacity: 1)
p.initialize(to: 0) // 初始化
p.pointee = 32
print("\(p.pointee)") // 打印:32
p.deinitialize(count: 1) // 反初始化
p.deallocate()

以上例子中我们提供了一个存放32位整形的内存块,容量为1(即其容量为1个32位整形,实际就是 4 个字节)。 接下来的代码示例较为简单,即开辟内存,初始化、赋值、反初始化,释放内存的流程。

Swift指针类型和C指针类型的对应关系

Swift的指针类型看似繁多,事实上只是对C指针类型进行了封装和类别整理,并增加了一定程度上的安全性。

下表提供了SwiftC部分指针类型和函数的大致等价关系。

SwiftC描述
UnsafeMutableRawPointervoid*空指针
UnsafeMutablePointerT*类型指针
UnsafeRawPointerconst void*常量空指针
UnsafePointerconst T*常量类型指针
UnsafeMutablePointer.allocate(int32_t*)malloc分配内存

可以看出Swift的指针并不神秘,它只是映射了C语言指针的对应操作(只是乍看一下更复杂)。

进阶实践:C标准库函数的映射调用

Swift提供了大量的C标准库的桥接调用,也就是说,我们可以像调用C语言库函数一样调用Swift函数。这其中包括很多有用的函数,如memcpystrcpy等。

下面通过一段示例程序来展现这类函数的调用。

var n = 10086
// malloc
let p = malloc(MemoryLayout<Int32>.size)!
// memcpy
memcpy(p, &n, MemoryLayout<Int32>.size)
let p2 = p.assumingMemoryBound(to: Int32.self)
print("\(p2.pointee)") // 打印:10086
// strcpy
let str = "abc".cString(using: .ascii)!
if str.count != MemoryLayout<Int32>.size {
return
}
let pstr = p.assumingMemoryBound(to: CChar.self)
strcpy(pstr, str)
print("\(String(cString: pstr))") // 打印:abc
// strlen
print("\(strlen(pstr))") // 打印: 3
// memset
memset(p, 0, MemoryLayout<Int32>.size)
print("\(p2.pointee)") // 打印:0
// strcat
strcat(pstr, "h".cString(using: .ascii)!)
strcat(pstr, "i".cString(using: .ascii)!)
print("\(String(cString: pstr))") // 打印:hi
// strstr
let s = strstr(pstr, "i")!
print("\(String(cString: s))") // 打印:i
// strcmp
print("\(strcmp(pstr, "hi".cString(using: .ascii)!))") // 打印:0
// free
free(p)

以上demo提供了如memsetstrcpyC库函数原型的调用方式。通过该例子可以看出指针操作的灵活性,对于开辟的一块4个字节的内存,我们既可以把它看做一个32位整形,又可以把它看做4个ascii字符,当把它看做4个字符时,我们可以用它存放abc三个字符,并在最后一个字节用\0作为终止符。

总结

指针可以让我们用更底层的视角来看待程序和数据,在某些场景下,通过指针我们有机会开发出更高性能的代码。但同时指针的使用有时也是极复杂易出错的。如何使用好这把双刃剑,全看开发者自身的能力和态度。本文仅仅是抛砖引玉的提供了Swift指针的基本框架和使用技巧,大量细节因为篇幅原因并未提及,还需要读者自行不断研究和学习。

本文的样例代码已上传至我的github,请参见地址:github.com/FengHaiTong… 。


作者:风海铜锣
链接:https://juejin.cn/post/7030789069915291661
来源:稀土掘金

收起阅读 »

Swift热更新(1)- 免费版接入

iOS
SOT学习和使用的成本主要集中在前期,主要涉及编译流程的修改。之前介绍了纯OC项目如何接入「 OC接入例子 」。本文介绍如何给纯Swift项目接入SOT,包括免费版和网站版。本文以开源的「 SwiftMessages 」Demo为例,该工程全部用Swift语言...
继续阅读 »

SOT学习和使用的成本主要集中在前期,主要涉及编译流程的修改。之前介绍了纯OC项目如何接入「 OC接入例子 」。本文介绍如何给纯Swift项目接入SOT,包括免费版和网站版。

本文以开源的「 SwiftMessages 」Demo为例,该工程全部用Swift语言开发。这里把修改好的版本上传到了git上,分支为 「 sotdemo 」,Debug模式下接入了免费版,Release模式接入了网站版,读者也可以直接用该分支测试。

现在开始从头讲解,clone原本的工程后,命令行cd进入根目录,使用版本切换命令:git checkout 1e49de7b3780b699(因本文档制作于21年10月21号,以当日版本为准)。首先进入Demo目录,打开Demo.xcodeproj工程,scheme默认就已经选中了Demo:...

我使用的是Xcode12.4,可以直接编译成功,启动APP能看到画面(模拟器):

......

点击最上面的MESSAGE VIEW控件,会弹出一个错误提示窗口,今天我们就来用SOT热更的方式修改错误提示的文案:...

Step1: 配置编译环境

参考「 免费版 」的step1到step3,step3拷贝的sotconfig.sh放到项目的Demo的目录下:...

用文本编辑器打开sotconfig.sh,修改EnableSot=1:...

Step2: 修改编译选项

添加热更需要的编译选项,添加SOT虚拟机静态库等,步骤如下:

  1. 选中Demo工程,然后选择Demo这个Target,再选择Build Settings:...

  2. Other Linker Flags添加-sotmodule $(PRODUCT_NAME) /Users/sotsdk-1.0/libs/libsot_free.a -sotsaved $(SRCROOT)/sotsaved/$(CONFIGURATION)/$(CURRENT_ARCH) -sotconfig $(SRCROOT)/sotconfig.sh,每个选项的意义如下:

    • -sotmodule是module的名字,可以直接用$(PRODUCT_NAME),也可以自定义名字;
    • -sotsaved是编译中间产物保存的目录,补丁自动化生成需要对比前后编译的产物来生成补丁;
    • -sotconfig指定了项目sotconfig.sh的路径,该脚本控制sot编译器的工作,用$(SRCROOT)引用到
    • /Users/sotsdk-1.0/libs/libsot_free.a是SOT虚拟机静态库的路径,链接的是免费版的虚拟机
  3. Other C Flags以及Other Swift Flags添加-sotmodule $(PRODUCT_NAME) -sotconfig $(SRCROOT)/sotconfig.sh,意义跟上一步是一样的,需要保持一致。经过上面两步,相关的编译配置结果如下图:...

  4. 因为SOT SDK库文件编译时不带Bitcode,所以也需要把Target的Enable Bitcode设为No...


Step3: 增加拷贝补丁脚本

SDK里提供了一个便利脚本,路径在sdk目录的project-script/sot_package.sh,它会把生成的补丁拷贝到Bundle文件夹下,在每次项目编译成功时调用该脚本,添加步骤如下:

...

脚本内容为:sh /Users/sotsdk-1.0/project-script/sot_package.sh "$SOURCE_ROOT/sotconfig.sh" "$SOURCE_ROOT/sotsaved/$CONFIGURATION" Demo...

把Based on dependency analysis的勾去掉


Step4: 链接C++库

SOT需要压缩库和c++标准库的支持,还是在这个页面下,打开Link Binary With Libraries页...

点击加号,分别加入这两,libz.tbdlibc++.tbd...


Step5: 调用SDK API

需要用Swift代码调用OC代码,已经提供了一个样例代码在SDK的swift-call-objc目录中,可以直接添加到Demo工程中。点击Xcode软件的File按钮,接着点击Add Files to "Demo",如下图所示:...

选择到SDK目录swift-call-objc中,同时选中callsot.h和callsot.m两个文件,勾选下面的Copy items if needed,勾选Add to targets:中的Demo target,如下图所示:...

点击Add按钮,添加后会弹出询问:是否创建桥接文件。点击按钮Create Bridging Header:...

然后可以看到项目中多了3个文件,分别是callsot.h,callsot.m和Demo-Bridging-Header.h:...

打开Demo-Bridging-Header.h,加入一行代码#import "callsot.h"...

打开AppDelegate.swift,加入两行代码let sot = CallSot()sot.initSot()...


测试热更

Step1: 热更注入

按上面配置完之后,确保sotconfig.sh的配置是,EnableSot=1以及GenerateSotShip=0,先Clean Build Folder一下,然后再Build:...

然后看编译日志的输出,Link日志可以看到run sot link等输出,会告诉你每个文件里哪些函数可以被热更等信息:......

项目编译成功了,该APP可以正常启动。同时它具备了热更能力,可以加载补丁改变程序的代码逻辑,下面介绍如何生成补丁来测试它。


Step2: 生成补丁

上一步进行了热更注入的编译,当时的代码保存到了Demo/sotsaved这个文件夹下,用来和新代码比较生成补丁。生成补丁步骤如下:

  1. 首先启动SOT生成补丁模式,修改sotconfig.sh为EnableSot=1GenerateSotShip=1
  2. ...
  3. 接下来直接在Xcode里修改源代码,把ViewController.swift文件的”Something is horribly wrong!“改成了”SOT is great“:......
  4. 生成补丁跟OC项目不一样,每次都需要先Clean项目,再Build项目。然后查看编译日志输出,可以看到生成了补丁并且被脚本拷贝到了Bundle目录下,然后再展开Link Demo(x86_64)的编译日志:...可以看到此时的Link是用来生成补丁的,日志里也显示了函数demoBasics被修改了:...
  5. 生成出来的补丁原始文件保存到了Demo/sotsaved/Debug/x86_64/ship/ship.sot,还记得之前加了一个script到Build Phase中吗?它会每次编译结束时,会把这个补丁拷贝到了Bundle目录中,并且添加CPU架构到文件名中。可以在Bundle中看到这个补丁,至此完毕。

Step3: 加载补丁

启动APP,API会判断Bundle内是否有补丁,有则加载,加载成功的日志大概如下,提示有一个模块加载了热更补丁:...之后点击最上面的MESSAGE VIEW控件,发现弹出的文案变成了SOT is great:...

如果去Xcode断点调试demoBasics,会发现无法断住了,因为实际执行补丁代码的是SOT虚拟机。

顺便提一嘴,GenerateSotShip=1时,编译APP用的是保存在sotsaved目录下的代码,所以无论怎么修改Xcode里的代码,如果没有把补丁拷贝到Bundle目录里,那么APP都是最后一次GenerateSotShip=0热更注入时的样子。

如果怀疑,可以把拷贝补丁的Script脚本从Build Phases删除,可以发现怎么改代码都不会生效了。


作者:忒修斯科技
链接:https://juejin.cn/post/7026197659006287903
来源:稀土掘金

收起阅读 »

Swift开发规范

iOS
Swift开发规范前言开发规范的目的是保证统一项目成员的编码风格,并使代码美观,每个公司对于代码的规范也不尽相同,希望该份规范能给大家起到借鉴作用。本文为原创,如需转载请说明原文地址链接。命名规约代码中的命名严禁使用拼音及英文混合的方式,更不允许直接出现中文的...
继续阅读 »

Swift开发规范

前言

开发规范的目的是保证统一项目成员的编码风格,并使代码美观,每个公司对于代码的规范也不尽相同,希望该份规范能给大家起到借鉴作用。本文为原创,如需转载请说明原文地址链接。

命名规约

  • 代码中的命名严禁使用拼音及英文混合的方式,更不允许直接出现中文的方式,最好也不要使用下划线或者美元符号开头;
  • 文件名、class、struct、enum、protocol 命名统一使用 UpperCamelCase 风格;
  • 方法名、参数名、成员变量、局部变量、枚举成员统一使用 lowerCamelCase 风格
  • 全局常量命名使用 k 前缀 + UpperCamelCase 命名;
  • 扩展文件,用“原始类型名+扩展名”作为扩展文件名,其中原始类型名及扩展名也使用 UpperCamelCase 风格,如UIView+Frame.swift
  • 工程中文件夹或者 Group 统一使用 UpperCamelCase 风格,一律使用单数形式;
  • 命名中出现缩略词时,缩略词要么全部大写,要么全部小写,以首字母大小写为准,通用缩略词包括 JSON、URL 等;如class IDUtil {}func idToString() { }
  • 不要使用不规范的缩写,如 AbstractClass“缩写”命名成 AbsClass 等,不怕名称长,就怕名称不明确。
  • 文件名如果有复数含义,文件名应使用复数形式,如一些工具类;

修饰规约

  • 能用 let 修饰的时候,不要使用 var;
  • 修饰符顺序按照 注解、访问限制、static、final 顺序;
  • 尽可能利用访问限制修饰符控制类、方法等的访问限制;
  • 写方法时,要考虑这个方法是否会被重载。如果不会,标记为 final,final 会缩短编译时间;
  • 在编写库的时候需要注意修饰符的选用,遵循开闭原则;

格式规约

  • 类、函数左大括号不另起一行,与名称之间留有空格
  • 禁止使用无用分号
  • 代码中的空格出现地点
    • 注释符号与注释内容之间有空格
    • 类继承, 参数名和类型之间等, 冒号前面不加空格, 但后面跟空格
    • 任何运算符前后有空格
    • 表示返回值的 -> 两边
    • 参数列表、数组、tuple、字典里的逗号后面有一个空格
  • 方法之间空一行
  • 重载的声明放在一起,按照参数的多少从少到多向下排列
  • 每一行只声明一个变量
  • 如果是一个很长的数字时,建议使用下划线按照语言习惯三位或者四位一组分割连接。
  • 表示单例的静态属性,一般命名为 shared 或者 default
  • 如果是空的 block,直接声明{ },括号之间不需换行
  • 解包时推荐使用原有名字,前提是解包后的名字与解包前的名字在作用域上不会形成冲突
  • if 后面的 else\else if, 跟着上一个 if\else if 的右括号
  • switch 中, case 跟 switch 左对齐
  • 每行代码长度应小于 100 个字符,或者阅读时候不应该需要滚动屏幕,在正常范围内可以看到完整代码
  • 实现每个协议时, 在单独的 extension 里来实现

简略规约

  • Swift 会被结构体按照自身的成员自动生成一个非 public 的初始化方法,如果这个初始化方法刚好适合,不要自己再声明
  • 类及结构体初始化方法不要直接调用.init,直接直接省略,使用()
  • 如果只有一个 get 的计算属性,忽略 get
  • 数据定义时,尽量使用字面量形式进行自动推断,如果上下文不足以推断字面量类型时,需要声明赋值类型
  • 省略默认的访问权限(internal)
  • 过滤, 转换等, 优先使用 filter, map 等高阶函数简化代码,并尽量使用最简写
  • 使用闭包时,尽量使用最简写
  • 使用枚举属性时尽量使用自动推断,进行缩写
  • 无用的代码及时删除
  • 尽量使用各种语法糖
  • 访问实例成员或方法时尽量不要使用 self.,特殊场景除外,如构造函数时
  • 当方法无返回值时,不需添加 void

注释规约

  • 文档注释使用单行注释,即///,不使用多行注释,即/***/。 多行注释用于对某一代码段或者设计进行描述
  • 对于公开的类、方法以及属性等必须加上文档注释,方法需要加上对应的Parameter(s)ReturnsThrows 标签,强烈建议使用⌥ ⌘ /自动生成文档模板
  • 在代码中灵活的使用一些地标注释,如MARKFIXMETODO,当同一文件中存在多种类型定义或者多种逻辑时,可以使用Mark进行分组注释
  • 尽量将注释另起一行,而不是放在代码后

其他

  • 不要使用魔法值(即未经定义的常量);
  • 函数参数最多不得超过 8 个;寄存器数目问题,超过 8 个会影响效率;
  • 图形化的字面量,#colorLiteral(...)#imageLiteral(...)只能用在 playground 当做自我练习使用,禁止在项目工程中使用
  • 避免强制解包以及强制类型映射,尽量使用if let 或 guard let进行解包,禁止try!形式处理异常,避免使用隐式解包
  • 避免判断语句嵌套层次太深,使用 guard 提前返回
  • 如果 for 循环在函数体中只有一个 if 判断,使用 for where 进行替换
  • 实现每个协议时, 尽量在单独的 extension 里来实现;但需要考虑到协议的方法是否有 override 的可能,定义在 extension 的方法无法被 override,除非加上@objc 方法修改其派发方式
  • 优先创建函数而不是自定义操作符
  • 尽可能少的使用全局命名空间,如常量、变量、方法等
  • 赋值数组、字典时每个元素分别占用一行时,最后一个选项后面也添加逗号;这样未来如果有元素加入会更加方便
  • 布尔类型属性使用 is 作为属性名前缀,返回值为布尔型类型的方法名使用 is 作为方法名作为前缀
  • 类似注解的修饰词单独占一行,如@objc,@discardableResult 等
  • extension 上不用加任何修饰符,修饰符加在 extension 内的变量或方法上
  • 使用 guard 来提前结束条件,避免形成判断嵌套;
  • 善用字典去减少判断,可将条件与结果分别当做 key 及 value 存入字典中;
  • 封装时善用 assert,方便问题排查;
  • 在闭包中使用 self 时使用捕获列表[weak self]避免循环引用,闭包开始判断 self 的有效性
  • 使用委托和协议时,避免循环引用,定义属性的时候使用 weak 修饰

工具

SwiftLint 工具 提示格式错误

SwiftFormat 工具 提示并修复格式错误

两者大部分格式规范都是一致的,少许规范不一致,两个工具之间使用不冲突,可以在项目中共存。我们通过配置文件可以控制启用或者关闭相应的规则,具体使用规则参照对应仓库的 REAMME.md 文件。

相关规范

Swift 官方 API 设计指南

google 发布的 Swift 编码规范


有一个技术的圈子与一群同道众人非常重要,来我的技术公众号及博客,这里只聊技术干货。


链接:https://juejin.cn/post/6976282985695969294
收起阅读 »

? 我的独立开发的故事

iOS
🐻 我的独立开发的故事我是独立开发者熊大,最近一年尝试了独立开发的滋味,也想和大家聊一聊独立开发的心历路程。 如果你也有开发一款app的想法,那你可以看一看我的独立开发的故事。我做过直播、相机、社交类APP。个人独立app 《imi》《今日计划》2020年,我...
继续阅读 »

🐻 我的独立开发的故事

我是独立开发者熊大,最近一年尝试了独立开发的滋味,也想和大家聊一聊独立开发的心历路程。 如果你也有开发一款app的想法,那你可以看一看我的独立开发的故事。

  • 我做过直播、相机、社交类APP。
  • 个人独立app 《imi》《今日计划》
  • 2020年,我想要尝试一下独立开发的方向。

第一款app的开发周期

做第一款软件《今日计划》时,周一到周六工作,大小周,晚上会有一些开发时间。

总体如下:

  • 每天1小时写app代码 * 60 = 60小时
  • 每周周日有4个小时 * 8 = 32小时
  • 清明节三天 (按照8小时/天tian计算):3*8 = 24小时

一共约120个小时:完成了设计到上线。

我也买了阿里云的ECS,用vapor搭建了后台,维护成本有点高,果断放弃了。

当我开心的把它分享给朋友时,朋友们都说他很丑,于是被贴上一系列标签『丑』、『直男审美』、『搭配有问题』、『太简单了吧』····,总而言之,没什么好的形容词。

(PS:T M D 我自己都感觉有点坑)

 报着期望,又紧急改版一次,更换了icon,改了一些设计。也就是现在的这一版。我在圈子里又推广了一波,登顶效率榜Top20(其实是各位兄弟给面子)。

后来陆陆续续也有一些下载,但由于工作紧张,没能持续更新迭代。

离职风波

《水印相机》这款app目前,摄影榜Top20,很荣幸是我从零带到百万日活的,深知好产品的指数爆发增长。我内心真的想去外边看看,想见识更多优秀的、有趣的人,于是世界那么大,我想出去看看,真的成为了我离职的最主要理由。

从上家公司收获的最大的便是经验,一份让我受用很多年的经验。

离职后,并不缺少内推的机会,但我还没想好该怎么走接下来的路,我在思考,是去大厂深造,还是开启自由职业呢?自己一直是个骄傲的人,毕业时我的薪资就是 xx k,不能为五斗米而折腰,干脆做个自由职业好了。于是把想法讲给周围的人,最后还是找了份工作,公司就在我家的旁边,上下班5分钟。

于是从7月份开始,我就几乎每天晚上有两个小时的时间为开启我的自由职业之路做准备,只要副业收入过万,就开始全职独立开发。

新app上线

2020.08 一个小伙伴,会飞的猪,加入了开发阵营。

2020.10 小满 加了开发阵营。

(由于特殊原因,名字保密)

2021年1月上线了新的免费App《imi-成就最好的自己》,这次的app,至少在UI上取得了程序员的好评,我们还没有正式推广,只是在小圈子里发了一下动态试试水。

我们小团队也开了个新的公·众·号:《独立开发者基地》,感兴趣额可以关注。

惭愧的是,由于新公司较忙,进行了几次通宵加班后,我严重的拖累了小团队的开发进度,本来应该是2020年底就应该上线的。

《imi》

这是一款风格可爱简单的规划、计划类软件,致敬自己,致敬青春。

imi寓意:我就是我,我们一定是不完美的,也许不成功,也许不漂亮,但这就是我,与众不同。

给张图看看:

这个idea是我想的,简单说就是一个计划类软件,里边有

  • 人生节点
  • 座右铭
  • 成就
  • 笔记
  • 喜欢的人
  • 倒计时
  • 指纹解锁
  • 云同步。

设计这款软件希望能让大家觉得有用,不知道软件的初衷是不是个伪命题。让时间见证吧。

独立开发者应该都知道霸榜很久的《时间规划局》,这次《imi》就是冲着它去的,她将作为我们的竞品之一,我想我们这么有情怀的app对标这样的工具类软件,是有点希望的(怕怕)。

希望大家下载: imi-成就 给予我们支持 ^_^

给独立开发者的福利

这个应该算是福利吧,我们小团队,整理出了app的加速库,《今日计划》《imi-成就》两款app都是基于这个加速库开发的。接下来的其他app也会基于这个加速库开发,意味着我们会持续完善、维护这个加速库。里边有很多实用的功能,欢迎star🌟。

加速库SpeedySwift仓库:https://github.com/Tliens/SpeedySwift

imi 中用到的第三方库:

  # Pods for App1125
pod 'HWPanModal', '~> 0.8.1'
pod 'RealmSwift', '~> 10.5.0'
pod 'ZLPhotoBrowser', '~> 4.1.2'
pod 'SwiftDate', '~> 6.3.1'
pod 'IceCream',:path =>'Dev-pods/IceCream' # 数据同步icloud
# pod 'FSPagerView' # 轮播图
# pod 'SwiftyStoreKit' # 内购组件
pod 'Schedule', '~> 2.1.0'
pod 'Hero', '~> 1.5.0'
pod 'BiometricAuthentication'
#依赖库
pod 'UMCCommon', '~> 2.1.4'
#统计 SDK
pod 'UMCAnalytics', '~> 6.1.0'


回顾2020

get的技能:

  • 有幸能主导组件化开发
  • 函数响应式编程
  • go服务端

展望2021

希望大家健康、开心

我们会继续维护,维护今日计划、imi。也会有新的app出现。

最后

天行健君子以自强不息,地势坤君子以厚德载物。

虽大部分努力都没有收获,但热爱诞生创造的婴孩。

与君共勉!!!

写于 2021.01.13 北京·安贞门
链接:https://juejin.cn/post/6917058456184684557
收起阅读 »

python协程(超详细)

1、迭代1.1 迭代的概念使用for循环遍历取值的过程叫做迭代,比如:使用for循环遍历列表获取值的过程# Python 中的迭代for value in [2, 3, 4]:    print(value)1.2 可迭代对象标准概念:在类...
继续阅读 »



1、迭代

1.1 迭代的概念

使用for循环遍历取值的过程叫做迭代,比如:使用for循环遍历列表获取值的过程

# Python 中的迭代
for value in [2, 3, 4]:
   print(value)

1.2 可迭代对象

标准概念:在类里面定义__iter__方法,并使用该类创建的对象就是可迭代对象

简单记忆:使用for循环遍历取值的对象叫做可迭代对象, 比如:列表、元组、字典、集合、range、字符串

1.3 判断对象是否是可迭代对象

# 元组,列表,字典,字符串,集合,range都是可迭代对象
from collections import Iterable
# 如果解释器提示警告,就是用下面的导入方式
# from collections.abc import Iterable

# 判断对象是否是指定类型
result = isinstance((3, 5), Iterable)
print("元组是否是可迭代对象:", result)

result = isinstance([3, 5], Iterable)
print("列表是否是可迭代对象:", result)

result = isinstance({"name": "张三"}, Iterable)
print("字典是否是可迭代对象:", result)

result = isinstance("hello", Iterable)
print("字符串是否是可迭代对象:", result)

result = isinstance({3, 5}, Iterable)
print("集合是否是可迭代对象:", result)

result = isinstance(range(5), Iterable)
print("range是否是可迭代对象:", result)

result = isinstance(5, Iterable)
print("整数是否是可迭代对象:", result)

# 提示: 以后还根据对象判断是否是其它类型,比如以后可以判断函数里面的参数是否是自己想要的类型
result = isinstance(5, int)
print("整数是否是int类型对象:", result)

class Student(object):
   pass

stu = Student()
result = isinstance(stu, Iterable)

print("stu是否是可迭代对象:", result)

result = isinstance(stu, Student)

print("stu是否是Student类型的对象:", result)

1.4 自定义可迭代对象

在类中实现__iter__方法

自定义可迭代类型代码

from collections import Iterable
# 如果解释器提示警告,就是用下面的导入方式
# from collections.abc import Iterable

# 自定义可迭代对象: 在类里面定义__iter__方法创建的对象就是可迭代对象
class MyList(object):

   def __init__(self):
       self.my_list = list()

   # 添加指定元素
   def append_item(self, item):
       self.my_list.append(item)

   def __iter__(self):
       # 可迭代对象的本质:遍历可迭代对象的时候其实获取的是可迭代对象的迭代器, 然后通过迭代器获取对象中的数据
       pass

my_list = MyList()
my_list.append_item(1)
my_list.append_item(2)
result = isinstance(my_list, Iterable)

print(result)

for value in my_list:
   print(value)

执行结果:

Traceback (most recent call last):
True
 File "/Users/hbin/Desktop/untitled/aa.py", line 24, in <module>
   for value in my_list:
TypeError: iter() returned non-iterator of type 'NoneType'

通过执行结果可以看出来,遍历可迭代对象依次获取数据需要迭代器

总结

在类里面提供一个__iter__创建的对象是可迭代对象,可迭代对象是需要迭代器完成数据迭代的

2、迭代器

2.1 自定义迭代器对象

自定义迭代器对象: 在类里面定义__iter____next__方法创建的对象就是迭代器对象

from collections import Iterable
from collections import Iterator

# 自定义可迭代对象: 在类里面定义__iter__方法创建的对象就是可迭代对象
class MyList(object):

   def __init__(self):
       self.my_list = list()

   # 添加指定元素
   def append_item(self, item):
       self.my_list.append(item)

   def __iter__(self):
       # 可迭代对象的本质:遍历可迭代对象的时候其实获取的是可迭代对象的迭代器, 然后通过迭代器获取对象中的数据
       my_iterator = MyIterator(self.my_list)
       return my_iterator


# 自定义迭代器对象: 在类里面定义__iter__和__next__方法创建的对象就是迭代器对象
class MyIterator(object):

   def __init__(self, my_list):
       self.my_list = my_list

       # 记录当前获取数据的下标
       self.current_index = 0

       # 判断当前对象是否是迭代器
       result = isinstance(self, Iterator)
       print("MyIterator创建的对象是否是迭代器:", result)

   def __iter__(self):
       return self

   # 获取迭代器中下一个值
   def __next__(self):
       if self.current_index < len(self.my_list):
           self.current_index += 1
           return self.my_list[self.current_index - 1]
       else:
           # 数据取完了,需要抛出一个停止迭代的异常
           raise StopIteration


my_list = MyList()
my_list.append_item(1)
my_list.append_item(2)
result = isinstance(my_list, Iterable)

print(result)

for value in my_list:
   print(value)

运行结果:

True
MyIterator创建的对象是否是迭代器: True
1
2

2.2 iter()函数与next()函数

  1. iter函数: 获取可迭代对象的迭代器,会调用可迭代对象身上的__iter__方法

  2. next函数: 获取迭代器中下一个值,会调用迭代器对象身上的__next__方法

# 自定义可迭代对象: 在类里面定义__iter__方法创建的对象就是可迭代对象
class MyList(object):

   def __init__(self):
       self.my_list = list()

   # 添加指定元素
   def append_item(self, item):
       self.my_list.append(item)

   def __iter__(self):
       # 可迭代对象的本质:遍历可迭代对象的时候其实获取的是可迭代对象的迭代器, 然后通过迭代器获取对象中的数据
       my_iterator = MyIterator(self.my_list)
       return my_iterator


# 自定义迭代器对象: 在类里面定义__iter__和__next__方法创建的对象就是迭代器对象
# 迭代器是记录当前数据的位置以便获取下一个位置的值
class MyIterator(object):

   def __init__(self, my_list):
       self.my_list = my_list

       # 记录当前获取数据的下标
       self.current_index = 0

   def __iter__(self):
       return self

   # 获取迭代器中下一个值
   def __next__(self):
       if self.current_index < len(self.my_list):
           self.current_index += 1
           return self.my_list[self.current_index - 1]
       else:
           # 数据取完了,需要抛出一个停止迭代的异常
           raise StopIteration

# 创建了一个自定义的可迭代对象
my_list = MyList()
my_list.append_item(1)
my_list.append_item(2)

# 获取可迭代对象的迭代器
my_iterator = iter(my_list)
print(my_iterator)
# 获取迭代器中下一个值
# value = next(my_iterator)
# print(value)

# 循环通过迭代器获取数据
while True:
   try:
       value = next(my_iterator)
       print(value)
   except StopIteration as e:
       break

2.3 for循环的本质

遍历的是可迭代对象

  • for item in Iterable 循环的本质就是先通过iter()函数获取可迭代对象Iterable的迭代器,然后对获取到的迭代器不断调用next()方法来获取下一个值并将其赋值给item,当遇到StopIteration的异常后循环结束。

遍历的是迭代器

  • for item in Iterator 循环的迭代器,不断调用next()方法来获取下一个值并将其赋值给item,当遇到StopIteration的异常后循环结束。

2.4 迭代器的应用场景

我们发现迭代器最核心的功能就是可以通过next()函数的调用来返回下一个数据值。如果每次返回的数据值不是在一个已有的数据集合中读取的,而是通过程序按照一定的规律计算生成的,那么也就意味着可以不用再依赖一个已有的数据集合,也就是说不用再将所有要迭代的数据都一次性缓存下来供后续依次读取,这样可以节省大量的存储(内存)空间。

举个例子,比如,数学中有个著名的斐波拉契数列(Fibonacci),数列中第一个数为0,第二个数为1,其后的每一个数都可由前两个数相加得到:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...

现在我们想要通过for...in...循环来遍历迭代斐波那契数列中的前n个数。那么这个斐波那契数列我们就可以用迭代器来实现,每次迭代都通过数学计算来生成下一个数。

class Fibonacci(object):

   def __init__(self, num):
       # num:表示生成多少fibonacci数字
       self.num = num
       # 记录fibonacci前两个值
       self.a = 0
       self.b = 1
       # 记录当前生成数字的索引
       self.current_index = 0

   def __iter__(self):
       return self

   def __next__(self):
       if self.current_index < self.num:
           result = self.a
           self.a, self.b = self.b, self.a + self.b
           self.current_index += 1
           return result
       else:
           raise StopIteration


fib = Fibonacci(5)
# value = next(fib)
# print(value)

for value in fib:
   print(value)

执行结果:

0
1
1
2
3

小结

迭代器的作用就是是记录当前数据的位置以便获取下一个位置的值

3、生成器

3.1 生成器的概念

生成器是一类特殊的迭代器,它不需要再像上面的类一样写__iter__()和__next__()方法了, 使用更加方便,它依然可以使用next函数和for循环取值

3.2 创建生成器方法1

  • 第一种方法很简单,只要把一个列表生成式的 [ ] 改成 ( )

my_list = [i * 2 for i in range(5)]
print(my_list)

# 创建生成器
my_generator = (i * 2 for i in range(5))
print(my_generator)

# next获取生成器下一个值
# value = next(my_generator)
#
# print(value)
for value in my_generator:
   print(value)

执行结果:

[0, 2, 4, 6, 8]
<generator object <genexpr> at 0x101367048>
0
2
4
6
8

3.3 创建生成器方法2

在def函数里面看到有yield关键字那么就是生成器

def fibonacci(num):
   a = 0
   b = 1
   # 记录生成fibonacci数字的下标
   current_index = 0
   print("--11---")
   while current_index < num:
       result = a
       a, b = b, a + b
       current_index += 1
       print("--22---")
       # 代码执行到yield会暂停,然后把结果返回出去,下次启动生成器会在暂停的位置继续往下执行
       yield result
       print("--33---")


fib = fibonacci(5)
value = next(fib)
print(value)
value = next(fib)
print(value)

value = next(fib)
print(value)

# for value in fib:
#     print(value)

在使用生成器实现的方式中,我们将原本在迭代器__next__方法中实现的基本逻辑放到一个函数中来实现,但是将每次迭代返回数值的return换成了yield,此时新定义的函数便不再是函数,而是一个生成器了。

简单来说:只要在def中有yield关键字的 就称为 生成器

3.4 生成器使用return关键字

def fibonacci(num):
a = 0
b = 1
# 记录生成fibonacci数字的下标
current_index = 0
print("--11---")
while current_index < num:
result = a
a, b = b, a + b
current_index += 1
print("--22---")
# 代码执行到yield会暂停,然后把结果返回出去,下次启动生成器会在暂停的位置继续往下执行
yield result
print("--33---")
return "嘻嘻"

fib = fibonacci(5)
value = next(fib)
print(value)
# 提示: 生成器里面使用return关键字语法上没有问题,但是代码执行到return语句会停止迭代,抛出停止迭代异常

# return 和 yield的区别
# yield: 每次启动生成器都会返回一个值,多次启动可以返回多个值,也就是yield可以返回多个值
# return: 只能返回一次值,代码执行到return语句就停止迭代

try:
value = next(fib)
print(value)
except StopIteration as e:
# 获取return的返回值
print(e.value)

提示:

  • 生成器里面使用return关键字语法上没有问题,但是代码执行到return语句会停止迭代,抛出停止迭代异常

3.5 yield和return的对比

  • 使用了yield关键字的函数不再是函数,而是生成器。(使用了yield的函数就是生成器)

  • 代码执行到yield会暂停,然后把结果返回出去,下次启动生成器会在暂停的位置继续往下执行

  • 每次启动生成器都会返回一个值,多次启动可以返回多个值,也就是yield可以返回多个值

  • return只能返回一次值,代码执行到return语句就停止迭代,抛出停止迭代异常

3.6 使用send方法启动生成器并传参

send方法启动生成器的时候可以传参数

def gen():
   i = 0
   while i<5:
       temp = yield i
       print(temp)
       i+=1

执行结果:

In [43]: f = gen()

In [44]: next(f)
Out[44]: 0

In [45]: f.send('haha')
haha
Out[45]: 1

In [46]: next(f)
None
Out[46]: 2

In [47]: f.send('haha')
haha
Out[47]: 3

In [48]:

**注意:如果第一次启动生成器使用send方法,那么参数只能传入None,一般第一次启动生成器使用next函数

小结

  • 生成器创建有两种方式,一般都使用yield关键字方法创建生成器

  • yield特点是代码执行到yield会暂停,把结果返回出去,再次启动生成器在暂停的位置继续往下执行

4、协程

4.1 协程的概念

协程,又称微线程,纤程,也称为用户级线程,在不开辟线程的基础上完成多任务,也就是在单线程的情况下完成多任务,多个任务按照一定顺序交替执行 通俗理解只要在def里面只看到一个yield关键字表示就是协程

协程是也是实现多任务的一种方式

协程yield的代码实现

简单实现协程

import time

def work1():
   while True:
       print("----work1---")
       yield
       time.sleep(0.5)

def work2():
   while True:
       print("----work2---")
       yield
       time.sleep(0.5)

def main():
   w1 = work1()
   w2 = work2()
   while True:
       next(w1)
       next(w2)

if __name__ == "__main__":
   main()

运行结果:

----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
...省略...

小结

协程之间执行任务按照一定顺序交替执行

5、greenlet

5.1 greentlet的介绍

为了更好使用协程来完成多任务,python中的greenlet模块对其封装,从而使得切换任务变的更加简单

使用如下命令安装greenlet模块:

pip3 install greenlet

使用协程完成多任务

import time
import greenlet


# 任务1
def work1():
for i in range(5):
print("work1...")
time.sleep(0.2)
# 切换到协程2里面执行对应的任务
g2.switch()


# 任务2
def work2():
for i in range(5):
print("work2...")
time.sleep(0.2)
# 切换到第一个协程执行对应的任务
g1.switch()


if __name__ == '__main__':
# 创建协程指定对应的任务
g1 = greenlet.greenlet(work1)
g2 = greenlet.greenlet(work2)

# 切换到第一个协程执行对应的任务
g1.switch()

运行效果

work1...
work2...
work1...
work2...
work1...
work2...
work1...
work2...
work1...
work2...

6、gevent

6.1 gevent的介绍

greenlet已经实现了协程,但是这个还要人工切换,这里介绍一个比greenlet更强大而且能够自动切换任务的第三方库,那就是gevent。

gevent内部封装的greenlet,其原理是当一个greenlet遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。

由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO

安装

pip3 install gevent

6.2 gevent的使用

import gevent

def work(n):
   for i in range(n):
       # 获取当前协程
       print(gevent.getcurrent(), i)

g1 = gevent.spawn(work, 5)
g2 = gevent.spawn(work, 5)
g3 = gevent.spawn(work, 5)
g1.join()
g2.join()
g3.join()

运行结果

<Greenlet "Greenlet-0" at 0x26d8c970488: work(5)> 0
<Greenlet "Greenlet-1" at 0x26d8c970598: work(5)> 0
<Greenlet "Greenlet-2" at 0x26d8c9706a8: work(5)> 0
<Greenlet "Greenlet-0" at 0x26d8c970488: work(5)> 1
<Greenlet "Greenlet-1" at 0x26d8c970598: work(5)> 1
<Greenlet "Greenlet-2" at 0x26d8c9706a8: work(5)> 1
<Greenlet "Greenlet-0" at 0x26d8c970488: work(5)> 2
<Greenlet "Greenlet-1" at 0x26d8c970598: work(5)> 2
<Greenlet "Greenlet-2" at 0x26d8c9706a8: work(5)> 2
<Greenlet "Greenlet-0" at 0x26d8c970488: work(5)> 3
<Greenlet "Greenlet-1" at 0x26d8c970598: work(5)> 3
<Greenlet "Greenlet-2" at 0x26d8c9706a8: work(5)> 3
<Greenlet "Greenlet-0" at 0x26d8c970488: work(5)> 4
<Greenlet "Greenlet-1" at 0x26d8c970598: work(5)> 4
<Greenlet "Greenlet-2" at 0x26d8c9706a8: work(5)> 4

可以看到,3个greenlet是依次运行而不是交替运行

6.3 gevent切换执行

import gevent

def work(n):
   for i in range(n):
       # 获取当前协程
       print(gevent.getcurrent(), i)
       #用来模拟一个耗时操作,注意不是time模块中的sleep
       gevent.sleep(1)

g1 = gevent.spawn(work, 5)
g2 = gevent.spawn(work, 5)
g3 = gevent.spawn(work, 5)
g1.join()
g2.join()
g3.join()

运行结果

<Greenlet at 0x7fa70ffa1c30: f(5)> 0
<Greenlet at 0x7fa70ffa1870: f(5)> 0
<Greenlet at 0x7fa70ffa1eb0: f(5)> 0
<Greenlet at 0x7fa70ffa1c30: f(5)> 1
<Greenlet at 0x7fa70ffa1870: f(5)> 1
<Greenlet at 0x7fa70ffa1eb0: f(5)> 1
<Greenlet at 0x7fa70ffa1c30: f(5)> 2
<Greenlet at 0x7fa70ffa1870: f(5)> 2
<Greenlet at 0x7fa70ffa1eb0: f(5)> 2
<Greenlet at 0x7fa70ffa1c30: f(5)> 3
<Greenlet at 0x7fa70ffa1870: f(5)> 3
<Greenlet at 0x7fa70ffa1eb0: f(5)> 3
<Greenlet at 0x7fa70ffa1c30: f(5)> 4
<Greenlet at 0x7fa70ffa1870: f(5)> 4
<Greenlet at 0x7fa70ffa1eb0: f(5)> 4

6.4 给程序打补丁

import gevent
import time
from gevent import monkey

# 打补丁,让gevent框架识别耗时操作,比如:time.sleep,网络请求延时
monkey.patch_all()


# 任务1
def work1(num):
   for i in range(num):
       print("work1....")
       time.sleep(0.2)
       # gevent.sleep(0.2)

# 任务1
def work2(num):
   for i in range(num):
       print("work2....")
       time.sleep(0.2)
       # gevent.sleep(0.2)



if __name__ == '__main__':
   # 创建协程指定对应的任务
   g1 = gevent.spawn(work1, 3)
   g2 = gevent.spawn(work2, 3)

   # 主线程等待协程执行完成以后程序再退出
   g1.join()
   g2.join()

运行结果

work1....
work2....
work1....
work2....
work1....
work2....

6.5 注意

  • 当前程序是一个死循环并且还能有耗时操作,就不需要加上join方法了,因为程序需要一直运行不会退出

示例代码

import gevent
import time
from gevent import monkey

# 打补丁,让gevent框架识别耗时操作,比如:time.sleep,网络请求延时
monkey.patch_all()


# 任务1
def work1(num):
   for i in range(num):
       print("work1....")
       time.sleep(0.2)
       # gevent.sleep(0.2)

# 任务1
def work2(num):
   for i in range(num):
       print("work2....")
       time.sleep(0.2)
       # gevent.sleep(0.2)



if __name__ == '__main__':
   # 创建协程指定对应的任务
   g1 = gevent.spawn(work1, 3)
   g2 = gevent.spawn(work2, 3)

   while True:
       print("主线程中执行")
       time.sleep(0.5)

执行结果:

主线程中执行work1....work2....work1....work2....work1....work2....主线程中执行主线程中执行主线程中执行..省略..
  • 如果使用的协程过多,如果想启动它们就需要一个一个的去使用join()方法去阻塞主线程,这样代码会过于冗余,可以使用gevent.joinall()方法启动需要使用的协程

    实例代码

 import time
import gevent

def work1():
   for i in range(5):
       print("work1工作了{}".format(i))
       gevent.sleep(1)

def work2():
   for i in range(5):
       print("work2工作了{}".format(i))
       gevent.sleep(1)


if __name__ == '__main__':
   w1 = gevent.spawn(work1)
   w2 = gevent.spawn(work2)
   gevent.joinall([w1, w2])  # 参数可以为list,set或者tuple

7、进程、线程、协程对比

7.1 进程、线程、协程之间的关系

  • 一个进程至少有一个线程,进程里面可以有多个线程

  • 一个线程里面可以有多个协程

关系图.png

7.2 进程、线程、线程的对比

  1. 进程是资源分配的单位

  2. 线程是操作系统调度的单位

  3. 进程切换需要的资源最大,效率很低

  4. 线程切换需要的资源一般,效率一般(当然了在不考虑GIL的情况下)

  5. 协程切换任务资源很小,效率高

  6. 多进程、多线程根据cpu核数不一样可能是并行的,但是协程是在一个线程中 所以是并发

小结

  • 进程、线程、协程都是可以完成多任务的,可以根据自己实际开发的需要选择使用

  • 由于线程、协程需要的资源很少,所以使用线程和协程的几率最大

  • 开辟协程需要的资源最少

作者:y大壮
来源:https://juejin.cn/post/6971037591952949256

收起阅读 »

android媲美微信扫码库

之前使用的是zxing封装的库,但是识别率和识别速度没法和微信比较,现在使用的Google开源识别库完全可以和微信媲美github:github.com/DyncKathlin…强烈推荐MIKit Barcode Scanning识别速度超快,基本上camer...
继续阅读 »



之前使用的是zxing封装的库,但是识别率和识别速度没法和微信比较,现在使用的Google开源识别库完全可以和微信媲美

github:github.com/DyncKathlin…

强烈推荐MIKit Barcode Scanning

识别速度超快,基本上camera抓取到二维码就能识别到其内容(这是重点)。
基于MIKit Barcode Scanning的识别库进行封装,操作简单。
支持识别多个二维码,条形码。
支持任意比例展示,可以1:2,1.5:2等,不会发生像拉伸变形。
使用camera,不是cameraX哦。

效果图


第一个是Google开源的,第二个是zxing开源的

使用方式

build.gradle引用

implementation 'com.github.dynckathline:barcode:2.5'

初始化和监听结果回调

        //构造出扫描管理器
      configViewFinderView(viewfinderView);
      mlKit = new MLKit(this, preview, graphicOverlay);
      //是否扫描成功后播放提示音和震动
      mlKit.setPlayBeepAndVibrate(true, true);
      //仅识别二维码
      BarcodeScannerOptions options =
              new BarcodeScannerOptions.Builder()
                      .setBarcodeFormats(
                              Barcode.FORMAT_QR_CODE,
                              Barcode.FORMAT_AZTEC)
                      .build();
      mlKit.setBarcodeFormats(null);
      mlKit.setOnScanListener(new MLKit.OnScanListener() {
          @Override
          public void onSuccess(List<Barcode> barcodes, @NonNull GraphicOverlay graphicOverlay, InputImage image) {
              showScanResult(barcodes, graphicOverlay, image);
          }

          @Override
          public void onFail(int code, Exception e) {

          }
      });

展示结果

private void showScanResult(List<Barcode> barcodes, @NonNull GraphicOverlay graphicOverlay, InputImage image) {
      if (barcodes.isEmpty()) {
          return;
      }

      mlKit.setAnalyze(false);
      CustomDialog.Builder builder = new CustomDialog.Builder(context);
      CustomDialog dialog = builder
              .setContentView(R.layout.barcode_result_dialog)
              .setLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
              .setOnInitListener(new CustomDialog.Builder.OnInitListener() {
                  @Override
                  public void init(CustomDialog customDialog) {
                      Button btnDialogCancel = customDialog.findViewById(R.id.btnDialogCancel);
                      Button btnDialogOK = customDialog.findViewById(R.id.btnDialogOK);
                      TextView tvDialogContent = customDialog.findViewById(R.id.tvDialogContent);
                      ImageView ivDialogContent = customDialog.findViewById(R.id.ivDialogContent);

                      Bitmap bitmap = null;
                      ByteBuffer byteBuffer = image.getByteBuffer();
                      if (byteBuffer != null) {
                          FrameMetadata.Builder builder = new FrameMetadata.Builder();
                          builder.setWidth(image.getWidth())
                                  .setHeight(image.getHeight())
                                  .setRotation(image.getRotationDegrees());
                          bitmap = BitmapUtils.getBitmap(byteBuffer, builder.build());
                      } else {
                          bitmap = image.getBitmapInternal();
                      }
                      if (bitmap != null) {
                          graphicOverlay.add(new CameraImageGraphic(graphicOverlay, bitmap));
                      } else {
                          ivDialogContent.setVisibility(View.GONE);
                      }
                      SpanUtils spanUtils = SpanUtils.with(tvDialogContent);
                      for (int i = 0; i < barcodes.size(); ++i) {
                          Barcode barcode = barcodes.get(i);
                          BarcodeGraphic graphic = new BarcodeGraphic(graphicOverlay, barcode);
                          graphicOverlay.add(graphic);
                          Rect boundingBox = barcode.getBoundingBox();
                          spanUtils.append(String.format("(%d,%d)", boundingBox.left, boundingBox.top))
                                  .append(barcode.getRawValue())
                                  .setClickSpan(i % 2 == 0 ? getResources().getColor(R.color.colorPrimary) : getResources().getColor(R.color.colorAccent), false, new View.OnClickListener() {
                              @Override
                              public void onClick(View v) {
                                  Toast.makeText(getApplicationContext(), barcode.getRawValue(), Toast.LENGTH_SHORT).show();
                              }
                          })
                                  .setBackgroundColor(i % 2 == 0 ? getResources().getColor(R.color.colorAccent) : getResources().getColor(R.color.colorPrimary))
                                  .appendLine()
                                  .appendLine();
                      }
                      spanUtils.create();
                      Bitmap bitmapFromView = loadBitmapFromView(graphicOverlay);
                      ivDialogContent.setImageBitmap(bitmapFromView);

                      btnDialogCancel.setOnClickListener(new View.OnClickListener() {
                          @Override
                          public void onClick(View v) {
                              customDialog.dismiss();
                              finish();
                          }
                      });
                      btnDialogOK.setOnClickListener(new View.OnClickListener() {
                          @Override
                          public void onClick(View v) {
                              customDialog.dismiss();
                              mlKit.setAnalyze(true);
                          }
                      });
                  }
              })
              .build();
  }

作者:KathLine
来源:https://juejin.cn/post/6972476138203381790

收起阅读 »

Android:这是一个让你心动的日期&时间选择组件

预览引入添加 JitPack repositoryallprojects { repositories { ... maven { url "https://jitpack.io" } }}添加 Gradle依赖depe...
继续阅读 »



预览




imgimgimg

引入

添加 JitPack repository

allprojects {
repositories {
...
maven { url "https://jitpack.io" }
}
}

添加 Gradle依赖

dependencies {
  ...
  implementation 'com.google.android.material:material:1.1.0' //为了防止不必要的依赖冲突,0.0.3开始需要自行依赖google material库
  implementation 'com.github.loperSeven:DateTimePicker:0.3.0'//此处不保证最新版本,最新版需前往文末github查看
}

开始使用

内置弹窗CardDatePickerDialog

最简单的使用方式

//kotlin
    CardDatePickerDialog.builder(this)
              .setTitle("SET MAX DATE")
              .setOnChoose {millisecond->
                 
              }.build().show()
//java
new CardDatePickerDialog.Builder(this)
              .setTitle("SET MAX DATE")
              .setOnChoose("确定", aLong -> {
                   //aLong = millisecond
                   return null;
              }).build().show();

所有可配置属性

  CardDatePickerDialog.builder(context)
              .setTitle("CARD DATE PICKER DIALOG")
              .setDisplayType(displayList)
              .setBackGroundModel(model)
              .showBackNow(true)
              .setPickerLayout(layout)
              .setDefaultTime(defaultDate)
              .setMaxTime(maxDate)
              .setMinTime(minDate)
              .setWrapSelectorWheel(false)
              .setThemeColor(color)
              .showDateLabel(true)
              .showFocusDateInfo(true)
              .setLabelText("年","月","日","时","分")
              .setOnChoose("选择"){millisecond->}
              .setOnCancel("关闭") {}
              .build().show()

可配置属性说明

  • 设置标题

fun setTitle(value: String)
  • 是否显示回到当前按钮

fun showBackNow(b: Boolean)
  • 是否显示选中日期信息

fun showFocusDateInfo(b: Boolean)
  • 设置自定义选择器

//自定义选择器Layout注意事详见 【定制 DateTimePicker】
fun setPickerLayout(@NotNull layoutResId: Int)
  • 显示模式

// model 分为:CardDatePickerDialog.CARD//卡片,CardDatePickerDialog.CUBE//方形,CardDatePickerDialog.STACK//顶部圆角
// model 允许直接传入drawable资源文件id作为弹窗的背景,如示例内custom
fun setBackGroundModel(model: Int)
  • 设置主题颜色

fun setThemeColor(@ColorInt themeColor: Int)
  • 设置显示值

fun setDisplayType(vararg types: Int)
fun setDisplayType(types: MutableList<Int>)
  • 设置默认时间

fun setDefaultTime(millisecond: Long)
  • 设置范围最小值

fun setMinTime(millisecond: Long)
  • 设置范围最大值

fun setMaxTime(millisecond: Long)
  • 是否显示单位标签

fun showDateLabel(b: Boolean)
  • 设置标签文字

/**
*示例
*setLabelText("年","月","日","时","分")
*setLabelText("年","月","日","时")
*setLabelText(month="月",hour="时")
*/
fun setLabelText(year:String=yearLabel,month:String=monthLabel,day:String=dayLabel,hour:String=hourLabel,min:String=minLabel)
  • 设置是否循环滚动

/**
*示例(默认为true)
*setWrapSelectorWheel(false)
*setWrapSelectorWheel(DateTimeConfig.YEAR,DateTimeConfig.MONTH,wrapSelector = false)
*setWrapSelectorWheel(arrayListOf(DateTimeConfig.YEAR,DateTimeConfig.MONTH),false)
*/
fun setWrapSelectorWheel()
  • 绑定选择监听

/**
*示例
*setOnChoose("确定")
*setOnChoose{millisecond->}
*setOnChoose("确定"){millisecond->}
*/
fun setOnChoose(text: String = "确定", listener: ((Long) -> Unit)? = null)
  • 绑定取消监听

/**
*示例
*setOnCancel("取消")
*setOnCancel{}
*setOnCancel("取消"){}
*/
fun setOnCancel(text: String = "取消", listener: (() -> Unit)? = null)

选择器 DateTimePicker

xml中

app:layout 为自定义选择器布局 可参考 定制 DateTimePicker

        <com.loper7.date_time_picker.DateTimePicker
           android:id="@+id/dateTimePicker"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           app:layout="@layout/layout_date_picker_segmentation"
           app:showLabel="true"
           app:textSize="16sp"
           app:themeColor="#FF8080" />

代码中

  • 设置监听

    dateTimePicker.setOnDateTimeChangedListener { millisecond ->  }

更多设置

  • 设置自定义选择器布局(注意:需要在dateTimePicker其他方法之前调用,否则其他方法将会失效)

 dateTimePicker.setLayout(R.layout.layout_date_picker_segmentation)//自定义layout resId
  • 设置显示状态

DateTimePicker支持显示 年月日时分 五个选项的任意组合,显示顺序以此为年、月、日、时、分,setDisplayType中可无序设置。

     dateTimePicker.setDisplayType(intArrayOf(
           DateTimeConfig.YEAR,//显示年
           DateTimeConfig.MONTH,//显示月
           DateTimeConfig.DAY,//显示日
           DateTimeConfig.HOUR,//显示时
           DateTimeConfig.MIN))//显示分
  • 设置默认选中时间

 dateTimePicker.setDefaultMillisecond(defaultMillisecond)//defaultMillisecond 为毫秒时间戳
  • 设置允许选择的最小时间

  dateTimePicker.setMinMillisecond(minMillisecond)
  • 设置允许选择的最大时间

  dateTimePicker.setMaxMillisecond(maxMillisecond)
  • 是否显示label标签(选中栏 年月日时分汉字)

  dateTimePicker.showLabel(true)
  • 设置主题颜色

  dateTimePicker.setThemeColor(ContextCompat.getColor(context,R.color.colorPrimary))
  • 设置字体大小

设置的字体大小为选中栏的字体大小,预览字体会根据字体大小等比缩放

  dateTimePicker.setTextSize(15)//单位为sp
  • 设置标签文字

  //全部
 dateTimePicker.setLabelText(" Y"," M"," D"," Hr"," Min")
 //指定
 dateTimePicker.setLabelText(min = "M")

定制 DateTimePicker

说明

DateTimePicker 主要由至多6个 NumberPicker 组成,所以在自定义布局时,根据自己所需的样式摆放 NumberPicker 即可。以下为注意事项

开始定制

  • DateTimePicker 至多支持6个 NumberPicker ,你可以在xml中按需摆放1-6个 NumberPicker

  • 为了让 DateTimePicker 找到 NumberPicker ,需要在xml中为 NumberPicker 指定 idtag,规则如下

/**
* year:np_datetime_year
* month:np_datetime_month
* day:np_datetime_day
* hour:np_datetime_hour
* minute:np_datetime_minute
* second:np_datetime_second
*/
android:id="@+id/np_datetime_year"  or  android:tag="np_datetime_year"
  • 使用定制UI

CardDatePickerDialog 中使用

fun setPickerLayout(@NotNull layoutResId: Int)

DateTimePicker 中使用

<com.loper7.date_time_picker.DateTimePicker
           android:id="@+id/dateTimePicker"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           app:layout="@layout/layout_date_picker_segmentation"
           />

或者

 dateTimePicker.setLayout(R.layout.layout_date_picker_segmentation)//自定义layout resId

XML示例

示例图片

imgimg

更高的拓展性

如果以上自定义并不能满足你的需求,你还可以定制你自己的 DateTimePicker , 可参照 DateTimePicker.kt 定义你想要属性以及在代码内编写你的UI逻辑。选择器的各种逻辑约束抽离在 DateTimeController.kt ,你的 DateTimePicker 只需让 DateTimeController.kt 绑定 NumberPicker 即可。比如:

DateTimeController().bindPicker(YEAR, mYearSpinner)
          .bindPicker(MONTH, mMonthSpinner)
          .bindPicker(DAY, mDaySpinner).bindPicker(HOUR, mHourSpinner)
          .bindPicker(MIN, mMinuteSpinner).bindPicker(SECOND, mSecondSpinner).build()

作者:LOPER7
来源:https://juejin.cn/post/6917909994985750535

收起阅读 »

你可以永远相信debugger,但是不能永远相信console.log

总结放前面:console.log在打印引用数据类型的时候表现和我们的预期不相符合是因为console.log打印的是引用数据类型的一个快照,因为浏览器或者我们异步代码的原因在快照之后修改了对应的内存空间的值,所以等我们展开打印浏览器通过指针重新访问内存空间的...
继续阅读 »

总结放前面:

console.log在打印引用数据类型的时候表现和我们的预期不相符合是因为console.log打印的是引用数据类型的一个快照,因为浏览器或者我们异步代码的原因在快照之后修改了对应的内存空间的值,所以等我们展开打印浏览器通过指针重新访问内存空间的时候会获得最新的值导致展开和不展开的表现不一致。

不知道各位大佬有没有遇到过这样的情况,我在代码里面console.log()了一个数组,然后打开浏览器控制台,看着是空的就像这样[],结果我点展开它里面又有值了,但是在代码打印的位置使用length或者获取数组里面的值都是不行的,🤯 就像下面这样:

let arr = [];
const setFun = () => {
   return new Promise((reslove) => {
       let arr1 = [1, 2, 3];
       setTimeout(() => {
           reslove(arr1);
      }, 2000)
  });
}
const getFun = async () => {
   let result = await setFun();
   result.forEach((item) => {
       arr.push(item);
  })
}
getFun();
console.log(arr);


或者说我在某处代码console.log()一个对象,明明控制台打印对象的某一个key是1,但是我展开这个对象里面的key居然是2,我在代码里面获取的也是2,就像下面这样:
不知道各位大佬遇到这样的情况是怎么个想法,反正我第一次遇到的时候我还以为是我的谷歌浏览器出问题了,擦💦我甚至都想卸载重装一波。后来动了动🧠,觉得可能是代码执行顺序的原因,所以我就在代码里面打了断点看了一下,在执行console.log()的时候arr的确是一个空的对象,对arr数组的操作是在console.log()执行之后才进行的。

所以说这到底是为什么呐?
其实这个还是和js的引用数据类型还有console.log()的设计有关系。我们都知道引用数据类型大体上可以说是由两部分组成:指针和内容,指针保存的内容就是一个内存地址的指向,指针一般都是基本数据类型保存在栈内存,内容就包含着这个引用数据类型的实际值一般保存在堆内存。😍 而console.log呐打印的时候只是打印了这个引用数据类型的一个快照,快照中的指针和内容都是照相的时候的内容,在console.log()之后,修改了这个引用数据类型,或者说在这之前修改的操作在一个异步的内容里面,当我们去看打印的时候,这个引用数据类型的内容可能就被修改了,但是因为快照的原因我们看到的还是以前的值。
然后当我们展开的时候,浏览器会利用指针去内存重新读取内容,因为快找的指针是没有发生变化的,所以就看到了改变之后内存,这就是为什么我们展开和不展开看到的结果是不一样的原因了。当然造成这样的原因不一定都是因为我们代码在异步里面操作这个引用数据类型。
还有就是浏览器在进行I/O的时候异步会提升性能,所有这就是为什么有时候我们写的同步代码依然会出现不一致的情况,就像我第二个图一样。
下面就验证一下我上面的想法,当我把上面的代码修改一下,直接替换:

let arr = [];
const setFun = () => {
   return new Promise((reslove) => {
       let arr1 = [1, 2, 3];
       setTimeout(() => {
           reslove(arr1);
      }, 2000)
  });
}
const getFun = async () => {
   let result = await setFun();
   arr = result; // 修改部分
}
getFun();
console.log(arr);

那么我们看到的结果就和上面不一样了,这个展开的表现是和不展开是一样的。
相信各位大佬也知道是啥原因了,因为这次直接替换,修改的是指针的指向并没有修改之前引用数据饿类型的内存空间,所以当我们展开的时候快照中指针保存的地址还是空的,这样我们看到的和看之前的想法就对应上了。
注:该问题只存在于打印引用数据类型,基本数据类型不会出现。

作者:江湖不渡i
来源:https://juejin.cn/post/7032504319584780325

收起阅读 »

别被你的框架框住了

我短暂的职业生涯被 React 充斥着。还没毕业前我从 Vue 2.x 入手开始学习框架,在一个我当时觉得还行现在回看完全不行的状态进了公司。然后开启了跟 React 死磕的状态,从 class 组件到函数式组件,从 Redux 到 Recoil,从 Antd...
继续阅读 »

我短暂的职业生涯被 React 充斥着。

还没毕业前我从 Vue 2.x 入手开始学习框架,在一个我当时觉得还行现在回看完全不行的状态进了公司。然后开启了跟 React 死磕的状态,从 class 组件到函数式组件,从 Redux 到 Recoil,从 Antd 到 MUI...

不久前一个呆了2年多的项目成功结束,接下来要去一个新项目,新项目要用 Angular,于是我开始告别从毕业就开始用的 React,开始学习这个大家少有提及的框架。

回顾这几年,要说 React 带给我最多的是什么,我觉得可能是思想,是一种编程范式。为了理解 React 新的函数式组件,我去学习 FP,但我并不是一个原教旨主义者,所以我当然也不认同你想学 FP 就得去学 Lisp 的说法。

在这期间我发现小黄书的作者 Kyle Simpson 也写了一本专门为 JSer 介绍 FP 的,书中前言部分我深以为然:

The way I see it, functional programming is at its heart about using patterns in your code that are well-known, understandable, and proven to keep away the mistakes that make code harder to understand.

是的,编程范式的作用是为了让人们更好地组织和理解代码,编程范式应该去服务写代码的人,而不是人去事无巨细地遵循编程范式的每一个规则,理解每一个晦涩难懂的概念。

I believe that programming is fundamentally about humans, not about code. I believe that code is first and foremost a means of human communication, and only as a side effect (hear my self-referential chuckle) does it instruct the computer.

敏捷需要以人为本,写代码其实也一样。我们要做的应该是理解编程范式本身以及它背后的作用,或许在未来的某天你会突然发现,原来我用了这么久的这个玩意儿有一个这么有意思的名字,亦或者你可能永远也解释不清楚那个概念到底是什么:

A monad is just a monoid in the category of endofunctors.

一个单子不过是自函子范畴上的幺半群

那是不是搞不懂我就不能玩 FP 了?然后我就得站在鄙视链底端,被 Haskell、Lisp 玩家们指着鼻子嘲笑:你们看那家伙,其实啥也不懂,他那也叫 FP?

这个问题我没有答案,或许可以留给大家来讨论。但是到这里我至少明白了 React Hooks 为什么要叫 "hook";为什么有一个 hook 叫 "useEffect";我也理解了为什么大家都说不要用 hook 去实现 class 组件的生命周期。

除了写好 React 本身,我也尝试了纯函数、偏函数、柯里化、组合和 Point-free 风格的代码,确实得到了一些好处,也确实带来了一些不便。

可能这些思想就是学习 React 带给我最大的 side effect 吧(笑。

与 React 准备 all in FP 相反的是,与 Angular 短暂接触的我发现它全面拥抱 OOP。与当时 React 从 class 组件切换到函数式组件一样,首先你得把编程范式思想完全转变过来才能很好地理解 Angular。这又促使我不得不去复习许多被我丢弃很久的 OOP 思想。

到这我不禁想起一次公司内 TDD 训练营,作业完成后去找 coach 讲解,讲解过程中 coach 讲到了抽象能力、隔离层、防腐层。那时我才发现自己 OO 的抽象能力和一起的后端小伙伴一比实在是差到不行,只有大学时候的能力。反思过后像是被 React 给“惯”坏了,几乎已经丢掉了这部分能力。

老实说我接触 React class 组件时间并不长,第一个项目只有短短几个月。后面两个项目虽然去写 Java 了,但是第一个都是一些修修补补的工作,更像是在做 DevOps,后来的项目去写 Java BFF,毫无抽象可言,全是数据 mapping。然后又进到了一个将“追求技术卓越”贯彻执行的项目,成了那批最早吃函数式组件螃蟹的人。

于是我接触 class 组件的时间就只有作为毕业生的那短短几个月而已。

然后当我看到 Angular 文档中的依赖注入时,我脑子只能零星蹦出一些概念:SOLID、解耦。别说细节,我甚至不知道我蹦出来的这些东西是不是对的。于是我又只能凭着自己的记忆去邮件里搜相关的博客大赛的文章。

我好像已经丢掉了 OOP 了。

种下一棵树最好的时间是十年前,其次是现在。

跳出all in FP 的 React 我发现世界不是非黑即白的。说是全面拥抱 OOP,但其实你可以很轻易的在 Angular 中发现 FP 的影子 -- 用 pipe 来处理数据,用 Rx 来处理请求。

既然是以人为本,编程范式本就不应该对立,它们明明可以互补,在自己擅长的领域处理自己擅长的事情,哪怕是同一个项目。看惯了两个阵营吵架的场景,好像这样的场景才是我想要的。

于是我又回忆起某天在项目上和大家讨论的项目分包问题,最后的结论是 OOP 的以对象和 domain 分包的策略在大多数时候要优于单纯的 FP 的方式。它能让功能更集中,让大家更容易找到自己想要找的东西。

但是回过头来静静思考,我虽然会好好学习 OOP,但是我目前大概率不会去深入学习相关的建模方法。因为在目前我的工作环境下,我没看到有前端同学需要深刻理解建模方法的场景,大多数情况浅尝辄止即可。

以我自身的经历来看,DDD 我看过也参加过培训,也跟着项目后端小伙伴在搭建项目时从零到一实践过。但是在实践不多的情况下,整个过程逃脱不了学了忘忘了学的魔咒。大概唯一的用处就是当我被抓到后端去干活能看懂他们为什么要这么组织代码,至于建模的那个过程,被抓去干活的我是大概率不会参与的。(当然如果你有相关的经历还请喷醒我,比如你作为偏前端的小伙伴就是要熟练掌握建模方法,不然工作就做不下去了)

不要被技术栈限制住了自己,其实以前一直对这句话一知半解,虽然可能现在的理解也没有很强。可是当你从一个框里跳出来以后,去思考画框这个人的想法,你可能能够得到一些不一样的思考。对于 Thoughtworker 来说学习一个新框架,一门新语言可能不是什么问题,那我们是不是可以更进一步,想想那些看起来“虚无缥缈”的东西呢。

别被你的框架框住你了。


作者:Teobler
来源:https://juejin.cn/post/7032467133611294733

收起阅读 »

12 个救命的 CSS 技巧

✨12 个救命的 CSS 技巧✨ 1. 使用 Shape-outside 在浮动图像周围弯曲文本它是一个允许设置形状的 CSS 属性。它还有助于定义文本流动的区域。css代码:.any-shape {  width: 300px...
继续阅读 »



✨12 个救命的 CSS 技巧✨

1. 使用 Shape-outside 在浮动图像周围弯曲文本

它是一个允许设置形状的 CSS 属性。它还有助于定义文本流动的区域。css代码:

.any-shape {
 width: 300px;
 float: left;
 shape-outside: circle(50%);
}

2. 魔法组合

这个小组合实际上可以防止你在 HTML 中遇到的大多数布局错误的问题。我们确实不希望水平滑块或绝对定位的项目做他们想做的事情,也不希望到处都是随机的边距和填充。所以这是你们的魔法组合。

* {
padding: 0;
margin: 0;
max-width: 100%;
overflow-x: hidden;
position: relative;
display: block;
}

有时“display:block”没有用,但在大多数情况下,你会将 <a><span> 视为与其他块一样的块。所以,在大多数情况下,它实际上会帮助你!

3. 拆分 HTML 和 CSS

这更像是一种“工作流程”类型的技巧。我建议你在开发时创建不同的 CSS 文件,最后才合并它们。例如,一个用于桌面,一个用于移动等。最后,你必须合并它们,因为这将有助于最大限度地减少您网站的 HTTP 请求数量。

同样的原则也适用于 HTML。如果你不是在 Gatsby 等 SPA 环境中进行开发,那么 PHP 可用于包含 HTML 代码片段。例如,你希望在单独的文件中保留一个“/modules”文件夹,该文件夹将包含导航栏、页脚等。因此,如果需要进行任何更改,你不必在每个页面上都对其进行编辑。模块化越多,结果就越好。

4. ::首字母

它将样式应用于块级元素的第一个字母。因此,我们可以从印刷或纸质杂志中引入我们熟悉的效果。如果没有这个伪元素,我们将不得不创建许多跨度来实现这种效果。例如:

这是如何做到的?代码如下:

p.intro:first-letter {
 font-size: 100px;
 display: block;
 float: left;
 line-height: .5;
 margin: 15px 15px 10px 0 ;
}

5. 四大核心属性

CSS 动画提供了一种相对简单的方法来在大量属性之间平滑过渡。良好的动画界面依赖于流畅流畅的体验。为了在我们的动画时间线中保持良好的性能,我们必须将我们的动画属性限制为以下四个核心:

  • 缩放 - transform:scale(2)

  • 旋转 - transform:rotate(180deg)

  • 位置 – transform:translateX(50rem)

  • 不透明度 - opacity: 0.5

边框半径、高度/宽度或边距等动画属性会影响浏览器布局方法,而背景、颜色或框阴影的动画会影响浏览器绘制方法。所有这些都会大大降低您的 FPS (FramesPerSecond)。您可以使用这些属性来产生一些有趣的效果,但应谨慎使用它们以保持良好的性能。

6. 使用变量保持一致

保持一致性的一个好方法是使用 CSS 变量或预处理器变量来预定义动画时间。

:root{ timing-base: 1000;}

在不定义单元的情况下设置基线动画或过渡持续时间为我们提供了在 calc() 函数中调用此持续时间的灵活性。此持续时间可能与我们的基本 CSS 变量不同,但它始终是对该数字的简单修改,并将始终保持一致的体验。

7. 圆锥梯度

有没有想过是否可以只使用 CSS 创建饼图?好消息是,您实际上可以!这可以使用 conic-gradient 函数来完成。此函数创建一个由渐变组成的图像,其中设置的颜色过渡围绕中心点旋转。您可以使用以下代码行执行此操作:

.piechart {
 background: conic-gradient(rgb(255, 132, 45) 0% 25%, rgb(166, 195, 209) 25% 56%, #ffb50d  56% 100%);
 border-radius: 50%;
 width: 300px;
 height: 300px;
}

8. 更改文本选择颜色

要更改文本选择颜色,我们使用 ::selection。它是一个伪元素,在浏览器级别覆盖以使用您选择的颜色替换文本突出显示颜色。使用光标选择内容后即可看到效果。

::selection {
    background-color: #f3b70f;
}

9. 悬停效果

悬停效果通常用于按钮、文本链接、站点的块部分、图标等。如果您想在有人将鼠标悬停在其上时更改颜色,只需使用相同的 CSS,但要添加 :hover到它并更改样式。这是您的方法;

.m h2{ 
   font-size:36px;
   color:#000;
   font-weight:800;
}
.m h2:hover{
   color:#f00;
}

当有人将鼠标悬停在 h2 标签上时,这会将您的 h2 标签的颜色从黑色更改为红色。它非常有用,因为如果您不想更改它,则不必再次声明字体大小或粗细。它只会更改您指定的任何属性。

10.投影

添加此属性可为透明图像带来更好的阴影效果。您可以使用给定的代码行执行此操作。

.img-wrapper img{
         width: 100% ;
         height: 100% ;
         object-fit: cover ;
         filter: drop-shadow(30px 10px 4px #757575);
}

11. 使用放置项居中 Div

居中 div 元素是我们必须执行的最可怕的任务之一。但不要害怕我的朋友,你可以用几行 CSS 将任何 div 居中。只是不要忘记设置display:grid; 对于父元素,然后使用如下所示的 place-items 属性。

main{
width: 100% ;
height: 80vh ;
display: grid ;
place-items: center center;
}

12. 使用 Flexbox 居中 Div

我们已经使用地点项目将项目居中。但是现在我们解决了一个经典问题,使用 flexbox 将 div 居中。为此,让我们看一下下面的示例:

<div>
<div></div>
</div>
.center {
display: flex;
align-items: center;
justify-content: center;
}

.center div {
width: 100px;
height: 100px;
border-radius: 50%;
background: #b8b7cd;
}

首先,我们需要确保父容器持有圆,即 flex-container。在它里面,我们有一个简单的 div 来制作我们的圆圈。我们需要使用以下与 flexbox 相关的重要属性:

  • display: flex; 这确保父容器具有 flexbox 布局。

  • align-items: center; 这可确保 flex 子项与横轴的中心对齐。

  • justify-content: center; 这确保 flex 子项与主轴的中心对齐。

之后,我们就有了常用的圆形 CSS 代码。现在这个圆是垂直和水平居中的,试试吧!

作者:海拥
来源:https://juejin.cn/post/7024372412632268813

收起阅读 »

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
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »