注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

谈一谈凑单页的那些优雅设计(下)

接 谈一谈凑单页的那些优雅设计(上)最终的成品如下,各个命令执行顺序一目了然▐ 多算法分流设计【RecommendEngine】:推荐引擎,用于推荐feeds流业务逻辑封装【BaseDataEngine】:通用数据引擎,将引擎的通用层抽离出来,简化通...
继续阅读 »

接 谈一谈凑单页的那些优雅设计(上)

下方是一个使用例子:

public class CouponCustomCommand implements CommonCommand {
  @Override
  public boolean check(CartContext context) {
      // 如果不是跨店满减或者品类券,不进行该命令处理 
      return Objects.equals(BenefitEnum.kdmj, context.getRequestContext().getCouponData().getBenefitEnum())
          || Objects.equals(BenefitEnum.plCoupon, context.getRequestContext().getCouponData().getBenefitEnum());
  }

  @Override
  public boolean execute(CartContext context) {
      CartData cartData = context.getData();
      // 命令处理
      return true;
  }

最终的成品如下,各个命令执行顺序一目了然


多算法分流设计

上面讲完了底层的一些代码结构设计,接下来讲一讲针对业务层的代码设计。凑单分为很多个模块,推荐feeds流、榜单模块、秒杀模块、搜索模块。整体效果图如下:


针对这种不同模块使用不同的算法,我们最先能想到的设计就是每个模块都是一个单独的接口。各自组装各自的逻辑。但在实现过程中会发现,这其中有很多通用的逻辑,比如推荐feeds流和限时秒杀模块,使用的都是凑单引擎的,算法逻辑完全相同,只是多了获取秒杀key的逻辑,所以我会选择使用同一个接口,让该接口能够尽量的通用。这里我选用了策略工厂模式,核心类图如下:


【SeckillEngine】:秒杀引擎,用于秒杀模块业务逻辑封装

【RecommendEngine】:推荐引擎,用于推荐feeds流业务逻辑封装

【SearchEngine】:搜索引擎,用于搜索模块业务逻辑封装

【BaseDataEngine】:通用数据引擎,将引擎的通用层抽离出来,简化通用代码

【EngineFactory】:引擎工厂,用于模块路由到合适的引擎

该模式下,针对可能不断累加的模块,能完成快速的开发并投入使用,该模式也是比较通用,大家都会选择的模式,我这里就不再过多的业务阐述了,就讲讲我对策略模式的理解吧,一提到策略模式,有人就觉得,它的作用是避免 if-else 分支判断逻辑。实际上,这种认识是很片面的。策略模式主要的作用还是解耦,控制代码的复杂度,让每个部分都不至于过于复杂、代码量过多。除此之外,对于复杂代码来说,策略模式还能让其满足开闭原则,添加新策略的时候,最小化、集中化代码改动,减少引入 bug 的风险。

*P.S. 实际上,设计原则和思想比设计模式更加普适和重要。掌握了代码的设计原则和思想,我们能更清楚的了解,为什么要用某种设计模式,就能更恰到好处地应用设计模式。*



取巧的功能设计

凑单购物车部分

  • 设计的背景

凑单是跨店优惠工具使用链路上的核心环节,用户对凑单有很高的诉求,但目前由于凑单页不支持实时凑单进度提示等问题,导致用户凑单体验较差,亟需优化凑单体验进而提升流量转化效率。但由于某些原因,我们不得不独立开发一套凑单购物车,同时加上凑单进度,其中商品数据源以及动态计算能力还是使用的淘宝购物车。

  • 基本框架结构设计

凑单页购物车是需要展示满足某个跨店满减活动的商品(套购同理),我不能直接使用购物车的接口直接返回所有商品数据以及优惠明细。所以我这里将购物车的访问拆成了两个部分,第一步先通过购物车的data.query接口查询出该用户所有加购的商品(该商品数据只有id,数量,时间相关的信息)。在凑单页先进行一次活动商品过滤后,再将剩余的商品调用购物车的动态计算接口,完成整个凑单购物车内所有数据的展示。流程如下:


  • 分页排序设计

大促期间,购物车大部分加购的品都是满足跨店满减活动的,如果每次都所有的商品参与动态计算并一次返回,性能会非常的差,所以这里就需要做到分页,页面展示如果涉及到了分页,难度系数将成倍的上升。首先我们来看凑单购物车的排序需求:

  1. 首次进入凑单页商品的顺序需要和购物车保持一致

    同一个店铺的需要放在一起,按加购时间倒序排

    店铺间按最新加购的某个商品的加购时间倒序排

  2. 如果是从某个店铺点进来的,该店铺需要在凑单页置顶,并主动勾选

  3. 如果过程中发现有新加入的品,该品需要置顶(不用将该店铺的其他品置顶)

  4. 如果过程中发现有失效的商品需要沉底(放到最后一页并沉底)

  5. 如果过程中发现有失效的品转成生效,需移上来

难点分析

  1. 排序并不是简单的按照时间维度排,增加的店铺维度,以及店铺置顶的能力

  2. 我们没有自己的数据源,每次查出来都得重新排序

  3. 第一次进入的排序和后续新加购的商品排序不同

  4. 支持分页

技术方案

首先能想到的就是找个地方存一下排序好的顺序,第一选择肯定是使用redis,但根据评估如果按用户维度去存储商品顺序,亿级的用户量 * 活动量需要耗费几百G的缓存容量,同时还需要维护该缓存的生命周期,相对还是比较麻烦。这种用户维度的缓存最好是通过客户端来进行缓存,我该如何利用前端来做该缓存,并让前端无感知呢?以下是我的接口设计:

itemList[{"cartId": 11111,"quantity":50,"checked": 是否勾选}]当前所有前端的品
sign{}标志,前端不需要关注里面的东西,后端返回直接传,如果没有就不传
nexttrue是否继续加载
allCheckedtrue是否全选
handle[{"cartId":1111,"quantity": 5,"checked":true,"type": modify}]type=modify更新,checked勾选,nonChecked去掉勾选

其中sign对象服务端返回给前端,下一次请求需要将sign对象原封不动的传给服务端,sign中存储了分页信息,以及需要商品的排序,sign对象如下:

public class Sign {
  /**
    * 已加载到到权重
    */
  private Integer weight;

  /**
    * 本次查询购物车商品最晚加购时间
    */
  private Long endTime;

  /**
    * 上一次查询购物车所有排序好的商品
    */
  private List activityItemList;
}

具体方案

  1. 首次进入按商品加购时间以及店铺维度做好初始排序,并标记weight(第一个200,第二个199,依次类推),并保存在sign对象的activityItemList中,取第一页数据,并将该页最小weight和所有商品的最晚加购时间endTime同步记录到sign中。并将sign返回给前端

  2. 前端在加载下一页时将上次请求后端返回的sign字段重新传给后端,后端根据sign中的weight大小判断,依次取下一页数据,同时将最新的最小weight写入sign,返回给前端。

  3. 期间如果发现有商品的加购时间大于sign中的endTime,则主动将其置顶,weight使用默认最大数字200。

  4. 由于在排序时无法知道商品是否失效以及能够勾选,所以需要在商品补全后(调用购物车的动态计算接口)重新对失效商品排序。

    如果本页没有失效的品,不做处理

    如果本页全是失效的品,不做处理(为了处理最后几页都是失效品的情况)

    如果有下一页,将失效的品放到后面页沉底

    如果当前页是最后一页,则直接沉底

方案时序图如下:


  • 商品勾选设计

购物车的商品勾选后就会出现勾选商品的下单价格以及能享受的各类优惠,勾选情况主要分为:

  1. 勾选、反勾选、全选

  2. 全选情况下加载下一页

  3. 勾选的商品数量变化

效果图如下:


难点

  1. 勾选的品越多,动态计算的rt越长,当50个品一起勾选,页面接口返回时间将近1.5s

  2. 全选的情况下,下拉加载需要将新加载出来的品主动勾选上

  3. 尽可能的减少调用动态计算(比如加载非勾选的品,修改非勾选的商品数量)

设计方案

  1. 由于可能需要计算所有勾选的商品,所以前端需要将当前所有已加载的商品数据的勾选状态告知服务端

  2. 超过50个勾选商品时,不再调用动态计算接口,直接用本地价格计算总价,同时降级优惠明细和凑单进度

  3. 前端根据后端返回结果进行合并操作,减少不必要的计算开销

整体逻辑如下:


同时针对勾选处理,我将各类获取商品信息的动作封装进领域模型中(比如已勾选品,全部品,下一页品,操作的品,方便复用,⬆️代码设计已经讲过),获取各类商品的逻辑代码如下:

List activityItemList = cartData.getActivityItemList();
Map alreadyMap = requestContext.getAlreadyMap();
Map checkedItemMap = requestContext.getCheckedItemMap();
Map addNextItemMap = Optional.ofNullable(cartData.getAddNextItemList())
  .map(o -> o.stream().collect(Collectors.toMap(CartItemData::getCartId, Function.identity())))
  .orElse(Collections.emptyMap());
Map checkedHandleMap = context.getCheckedHandleMap();
Map nonCheckedHandleMap = context.getNonCheckedHandleMap();
Map modifyHandleMap = context.getModifyHandleMap();

勾选处理的逻辑代码如下:

boolean calculateAllChecked = isCalculateAllChecked(context, activityItemList);
activityItemList.forEach(v -> {
  CartItemDetail cartItemDetail = CartItemDetail.build(v);
  // 新加入的品,加入动态计算列表,并勾选
  if (v.getLastAddTime() > context.getEndTime()) {
      cartItemDetail.setChecked(true);
      cartData.addCalculateItem(cartItemDetail);
      // 勾选操作的品,加入动态计算列表,并勾选
  } else if (checkedHandleMap.containsKey(v.getCartId())) {
      cartItemDetail.setChecked(true);
      cartData.addCalculateItem(cartItemDetail);
      // 取消勾选的品,加入动态计算列表,并去勾选
  } else if (nonCheckedHandleMap.containsKey(v.getCartId())) {
      cartItemDetail.setChecked(false);
      cartData.addCalculateItem(cartItemDetail);
      // 勾选商品的数量修改,加入动态计算
  } else if (modifyHandleMap.containsKey(v.getCartId())) {
      cartItemDetail.setChecked(modifyHandleMap.get(v.getCartId()).getChecked());
      cartData.addCalculateItem(cartItemDetail);
      // 加载下一页,加入动态计算,如果是全选动作下,则将该页商品勾选
  } else if (addNextItemMap.containsKey(v.getCartId())) {
      if (context.isAllChecked()) {
          cartItemDetail.setChecked(true);
      }
      cartData.addCalculateItem(cartItemDetail);
      // 判断是否需要将之前所有勾选的商品加入动态计算
  } else if (calculateAllChecked && checkedItemMap.containsKey(v.getCartId())) {
      cartItemDetail.setChecked(true);
      cartData.addCalculateItem(cartItemDetail);
  }
});

P.S. 这里可能有人会发现,这么多的if-else就觉得它是烂代码。如果 if-else 分支判断不复杂、代码不多,这并没有任何问题,毕竟 if-else 分支判断几乎是所有编程语言都会提供的语法,存在即有理由。遵循 KISS 原则,怎么简单怎么来,就是最好的设计。非得用策略模式,搞出 n 多类,反倒是一种过度设计。

营销商品引擎key设计

  • 设计的背景

跨店满减和品类券从引擎中筛选是通过couponTagId + couponValue来召回的,couponTagId是ump的活动id,couponValue则是记录了满减信息。随着需求的迭代,我们需要展示满足跨店满减并同时满足其他营销玩法(比如限时秒杀)的商品,这里我们已经能筛选出满足跨店满减的品,但如果筛选出当前正在生效的限时秒杀的品呢?


  • 详细索引设计

导购的召回主要依赖倒排索引,而我们秒杀商品召回的关键是正在生效,所以我的设想是将时间写入key中,就有了如下设计:

字段示例:mkt_fn_t_60_08200000_60

index例子描述
0mkt营销工具平台
1fn前N
2t前N分钟
360开始前60分钟为预热时间
4082000008月20号0点0分
560开始后60分钟为结束时间

使用方可以遍历当前所有key,本地计算出当前正在生效的key再进行召回,具体细节这里就不做阐述了



最后的总结

设计的初衷是提高代码质量

我们经常会讲到一个词:初心。这词说的其实就是,你到底是为什么干这件事。不管走多远、产品经过多少迭代、转变多少次方向,“初心”一般都不会随便改。实际上,写代码也是如此。应用设计模式只是方法,最终的目的是提高代码的质量。具体点说就是,提高代码的可读性、可扩展性、可维护性等。所的设计都是围绕着这个初心来做的。

所以,在做代码设计的时候,一定要先问下自己,为什么要这样设计,为什么要应用这种设计模式,这样做是否能真正地提高代码质量,能提高代码质量的哪些方面。如果自己很难讲清楚,或者给出的理由都比较牵强,那基本上就可以断定这是一种过度设计,是为了设计而设计。

设计的过程是先有问题后有方案

在设计的过程中,我们要先去分析代码存在的痛点,比如可读性不好、可扩展性不好等等,然后再针对性地利用设计模式去改善,而不是看到某个场景之后,觉得跟之前在某本书中看到的某个设计模式的应用场景很相似,就套用上去,也不考虑到底合不合适,最后如果有人问起了,就再找几个不痛不痒、很不具体的伪需求来搪塞,比如提高了代码的扩展性、满足了开闭原则等等。

设计的应用场景是复杂代码

设计模式的主要作用就是解耦,也就是利用更好的代码结构将一大坨代码拆分成职责更单一的小类,让其满足高内聚低耦合等特性。而解耦的主要目的是应对代码的复杂性。设计模式就是为了解决复杂代码问题而产生的。

因此,对于复杂代码,比如项目代码量多、开发周期长、参与开发的人员多,我们前期要多花点时间在设计上,越是复杂代码,花在设计上的时间就要越多。不仅如此,每次提交的代码,都要保证代码质量,都要经过足够的思考和精心的设计,这样才能避免烂代码。

相反,如果你参与的只是一个简单的项目,代码量不多,开发人员也不多,那简单的问题用简单的解决方案就好,不要引入过于复杂的设计模式,将简单问题复杂化。

持续重构能有效避免过度设计

应用设计模式会提高代码的可扩展性,但同时也会带来代码可读性的降低,复杂度的升高。一旦我们引入某个复杂的设计,之后即便在很长一段时间都没有扩展的需求,我们也不可能将这个复杂的设计删除,后面一直要背负着这个复杂的设计前行。

为了避免错误的预判导致过度设计,我比较喜欢持续重构的开发方法。持续重构不仅仅是保证代码质量的重要手段,也是避免过度设计的有效方法。我上面的核心流程处理的框架代码,也是在一次又一次的重构中才写出来的。

作者:鸣翰(郑健) 大淘宝技术

收起阅读 »

通过拦截 Activity的创建 实现APP的隐私政策改造

序言 最近因为政策收紧,现在要求APP必须在用户同意的情况下才能获取隐私信息。但是很多隐私信息的获取是第三方SDK获取的。而SDK的初始化一般都在application中。由于维护的项目多,如果贸然改动很有可能造成潜在的问题。所以想研究一个低侵入性的方案。在不...
继续阅读 »

序言


最近因为政策收紧,现在要求APP必须在用户同意的情况下才能获取隐私信息。但是很多隐私信息的获取是第三方SDK获取的。而SDK的初始化一般都在application中。由于维护的项目多,如果贸然改动很有可能造成潜在的问题。所以想研究一个低侵入性的方案。在不影响原有APP流程的基础上完成隐私改造。


方案


研究了几个方案,简单的说一下


方案1


通过给APP在设置一个入口,将原有入口的activity的enable设置为false。让客户端先进入到隐私确认界面
。确认完成,再用代码使这个activity的enable设置为false。将原来的入口设置为true。
需要的技术来自这篇文章
(技术)Android修改桌面图标


效果


这种方案基本能满足要求。但是存在两个问题。



  1. 将activity设置为false的时候会让应用崩溃。上一篇文章提到使用别名的方案也不行。

  2. 修改了activity以后,Android Studio启动的时候无法找到在清单文件中声明的activity。


方案2


直接Hook Activity的创建过程,如果用户没有通过协议,就将activity 变为我们的询问界面。
参考文献:
Android Hook Activity 的几种姿势


Android应用进程的创建 — Activity的启动流程


需要注意的是,我们只需要Hook ActivityThread 的mInstrumentation 即可。需要hook的方法是newActivity方法。


public class ApplicationInstrumentation extends Instrumentation {

private static final String TAG = "ApplicationInstrumentation";

// ActivityThread中原始的对象, 保存起来
Instrumentation mBase;

public ApplicationInstrumentation(Instrumentation base) {
mBase = base;
}

public Activity newActivity(ClassLoader cl, String className,
Intent intent)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
className = CheckApp.getApp().getActivityName(className);
return mBase.newActivity(cl, className, intent);
}
}

使用


最终使用了方案2。通过一个CheckApp类来实现管理。
使用很简单,将你的Application类继承自CheckApp 将sdk的初始化放置到 initSDK方法中
为了避免出错,在CheckApp中我已经将onCreate设置为final了


public class MyApp extends CheckApp {


public DatabaseHelper dbHelper;
protected void initSDK() {
RxJava1Util.setErrorNotImplementedHandler();
mInstance = this;
initUtils();
}

private void initUtils() {
}
}

在清单文件中只需要注册你需要让用户确认隐私协议的activity。


<application>
...
<meta-data
android:name="com.trs.library.check.activity"
android:value=".activity.splash.GuideActivity" />

</application>

如果要在应用每次升级以后都判断用户协议,只需要覆盖CheckApp中的这个方法。(默认开启该功能)


/**
* 是否每个版本都检查是否拥有用户隐私权限
* @return
*/
protected boolean checkForEachVersion() {
return true;
}

判断用户是否同意用这个方法


CheckApp.getApp().isUserAgree();

用户同意以后的回调,第二个false表示不自动跳转到被拦截的Activity


    /**
* 第二个false表示不自动跳转到被拦截的Activity
* CheckApp 记录了被拦截的Activity的类名。
*/
CheckApp.getApp().agree(this,false,getIntent().getExtras());

源码


一共只有3个类
在这里插入图片描述


ApplicationInstrumentation


import android.app.Activity;
import android.app.Instrumentation;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;

import java.lang.reflect.Method;

/**
* Created by zhuguohui
* Date: 2021/7/30
* Time: 13:46
* Desc:
*/
public class ApplicationInstrumentation extends Instrumentation {

private static final String TAG = "ApplicationInstrumentation";

// ActivityThread中原始的对象, 保存起来
Instrumentation mBase;

public ApplicationInstrumentation(Instrumentation base) {
mBase = base;
}

public Activity newActivity(ClassLoader cl, String className,
Intent intent)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
className = CheckApp.getApp().getActivityName(className);
return mBase.newActivity(cl, className, intent);
}


}

CheckApp




import android.app.Activity;
import android.app.ActivityManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.multidex.MultiDexApplication;

import com.trs.library.util.SpUtil;

import java.util.List;

/**
* Created by zhuguohui
* Date: 2021/7/30
* Time: 10:01
* Desc:检查用户是否给与权限的application
*/
public abstract class CheckApp extends MultiDexApplication {

/**
* 用户是否同意隐私协议
*/
private static final String KEY_USER_AGREE = CheckApp.class.getName() + "_key_user_agree";
private static final String KEY_CHECK_ACTIVITY = "com.trs.library.check.activity";

private boolean userAgree;

private static CheckApp app;


@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
userAgree = SpUtil.getBoolean(this, getUserAgreeKey(base), false);
getCheckActivityName(base);
if (!userAgree) {
//只有在用户不同意的情况下才hook ,避免性能损失
try {
HookUtil.attachContext();
} catch (Exception e) {
e.printStackTrace();
}
}
}


protected String getUserAgreeKey(Context base) {
if (checkForEachVersion()) {
try {
long longVersionCode = base.getPackageManager().getPackageInfo(base.getPackageName(), 0).versionCode;
return KEY_USER_AGREE + "_version_" + longVersionCode;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}
return KEY_USER_AGREE;

}

/**
* 是否每个版本都检查是否拥有用户隐私权限
* @return
*/
protected boolean checkForEachVersion() {
return true;
}

private static boolean initSDK = false;//是否已经初始化了SDK

String checkActivityName = null;

private void getCheckActivityName(Context base) {
mPackageManager = base.getPackageManager();
try {
ApplicationInfo appInfo = mPackageManager.getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
checkActivityName = appInfo.metaData.getString(KEY_CHECK_ACTIVITY);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
checkActivityName = checkName(checkActivityName);

}

public String getActivityName(String name) {
if (isUserAgree()) {
return name;
} else {
setRealFirstActivityName(name);
return checkActivityName;
}
}

private String checkName(String name) {
String newName = name;
if (!newName.startsWith(".")) {
newName = "." + newName;
}
if (!name.startsWith(getPackageName())) {
newName = getPackageName() + newName;
}

return newName;

}


@Override
public final void onCreate() {
super.onCreate();
if (!isRunOnMainProcess()) {
return;
}
app = this;
initSafeSDK();

//初始化那些和隐私无关的SDK
if (userAgree && !initSDK) {
initSDK = true;
initSDK();
}

}


public static CheckApp getApp() {
return app;
}


/**
* 初始化那些和用户隐私无关的SDK
* 如果无法区分,建议只使用initSDK一个方法
*/
protected void initSafeSDK() {

}


/**
* 判断用户是否同意
*
* @return
*/
public boolean isUserAgree() {
return userAgree;
}


static PackageManager mPackageManager;


private static String realFirstActivityName = null;

public static void setRealFirstActivityName(String realFirstActivityName) {
CheckApp.realFirstActivityName = realFirstActivityName;
}

public void agree(Activity activity, boolean gotoFirstActivity, Bundle extras) {

SpUtil.putBoolean(this, getUserAgreeKey(this), true);
userAgree = true;

if (!initSDK) {
initSDK = true;
initSDK();
}

//启动真正的启动页
if (!gotoFirstActivity) {
//已经是同一个界面了,不需要自动打开
return;
}
try {
Intent intent = new Intent(activity, Class.forName(realFirstActivityName));
if (extras != null) {
intent.putExtras(extras);//也许是从网页中调起app,这时候extras中含有打开特定新闻的参数。需要传递给真正的启动页
}
activity.startActivity(intent);
activity.finish();//关闭当前页面
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

}


/**
* 子类重写用于初始化SDK等相关工作
*/
abstract protected void initSDK();

/**
* 判断是否在主进程中,一些SDK中的PushServer可能运行在其他进程中。
* 也就会造成Application初始化两次,而只有在主进程中才需要初始化。
* * @return
*/
public boolean isRunOnMainProcess() {
ActivityManager am = ((ActivityManager) this.getSystemService(Context.ACTIVITY_SERVICE));
List<ActivityManager.RunningAppProcessInfo> processInfos = am.getRunningAppProcesses();
String mainProcessName = this.getPackageName();
int myPid = android.os.Process.myPid();
for (ActivityManager.RunningAppProcessInfo info : processInfos) {
if (info.pid == myPid && mainProcessName.equals(info.processName)) {
return true;
}
}
return false;
}
}

HookUtil



import android.app.Instrumentation;
import android.util.Log;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

/**
* Created by zhuguohui
* Date: 2021/7/30
* Time: 13:20
* Desc:
*/
public class HookUtil {



public static void attachContext() throws Exception {
Log.i("zzz", "attachContext: ");
// 先获取到当前的ActivityThread对象
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
//currentActivityThread是一个static函数所以可以直接invoke,不需要带实例参数
Object currentActivityThread = currentActivityThreadMethod.invoke(null);

// 拿到原始的 mInstrumentation字段
Field mInstrumentationField = activityThreadClass.getDeclaredField("mInstrumentation");
mInstrumentationField.setAccessible(true);
Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread);
// 创建代理对象
Instrumentation evilInstrumentation = new ApplicationInstrumentation(mInstrumentation);
// 偷梁换柱
mInstrumentationField.set(currentActivityThread, evilInstrumentation);
}


}

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

给灭霸点颜色看看

前言 继续我们 Flutter 绘图相关的介绍,本篇我们引入一位重量级主角 —— 灭霸。通过绘图的颜色过滤器,我们要给灭霸点颜色看看。通过本篇,你会了解到如下内容: ColorFilter 颜色过滤器的介绍; 彩色图片转换为灰度图; 通过矩阵运算构建自定义的...
继续阅读 »

前言


继续我们 Flutter 绘图相关的介绍,本篇我们引入一位重量级主角 —— 灭霸。通过绘图的颜色过滤器,我们要给灭霸点颜色看看。通过本篇,你会了解到如下内容:



  • ColorFilter 颜色过滤器的介绍;

  • 彩色图片转换为灰度图;

  • 通过矩阵运算构建自定义的颜色过滤器。


ColorFilter 颜色过滤器


其实我们之前在给小姐姐的照片调个颜色滤镜有介绍过颜色滤镜,在 Flutter 中提供了一个 ColorFiltered 的组件,可以将颜色过滤器应用到其子组件上。实际上,颜色过滤器就是对一个图层的每个像素的颜色(包括透明度)进行数学运算,改变像素的颜色来实现特定的效果。数学公式如下:


颜色变换矩阵


在 Flutter 中,ColorFilter 类的继承自 ImageFilter,像 ImageFilter 一样,也只提供了命名构造函数,一共有四个命名构造函数,分别如下:



  • ColorFilter.mode(Color color, BlendMode mode):按制定的混合模式(blend mode),将颜色混入到绘制的目标中。可以理解为图像的色值调整,我们可以用一个指定的颜色调整原图,调整的模式有很多种,具体可以查看 BlendMode 枚举。

  • ColorFilter.linearToSrgbGamma():将一个 SRGB 的 gamma 曲线应用到 RGB 颜色通道中。

  • ColorFilter.srgbToLinearGamma()ColorFilter.linearToSrgbGamma()的反向过程。

  • ColorFilter.matrix(List<double> matrix):应用一个矩阵做颜色变换,也就是我们上面说的矩阵,这是最通用的版本,要什么效果可以自己构建对应的矩阵。


这里说一下 SRGB 的 gamma 曲线的用途。我们人眼在显示屏中对图片进行调色等操作时,是按照线性空间的角度进行的,但显示器是在gamma空间中的,那么图像在计算机中的存储一般都应该是在 gamma 空间下了。也就是计算机存储的是非线性的,但是给我们展示的时候要转为线性的。因此,对于一张图像,可能是线性的也可能是 gamma 空间的,这个时候为了统一可能就需要进行转换,那就会用到linearToSrgbGammasrgbToLinearGamma两个颜色过滤器。


彩色图片转成灰色图片


彩色图片转变为灰色图片有很2种方法,最简单的方法是使用ColorFilter.mode,第一个参数颜色选择灰色或黑色,然后 第二个参数选择 BlendMode.color 或者接近的效果(比如 huesaturation)。BlendMode.color 是取源图的色调和饱和度,然后取目标(即要改变的图片)的亮度。因此,如果我们想更改一张图片的色调,用这种方式最好了。下面是对应的实现代码和变换前后的对比图。


var paint = Paint();
paint.colorFilter = ColorFilter.mode(Colors.grey, BlendMode.color);

canvas.drawImageRect(
bgImage,
Rect.fromLTRB(0, 0, bgImage.width.toDouble(), bgImage.height.toDouble()),
Offset.zero & size,
paint,
);

灰度图.jpg
使用ColorFilter.mode另一个用途就是简单的“修图”了,比如我们可以将一张蓝天白云图修成夕阳西下的效果。


夕阳效果.jpg


当然,转换为灰度图我们也可以通过矩阵实现。


矩阵运算改变颜色


如果要想任意调换颜色,那么使用矩阵运算更合适。在 Flutter 中,ColorFilter.matrix 多增加了一行,这一行主要是在构建一些特殊的矩阵运算更方便,比如反转色的时候。


Matrix 构建公式


比如我们要让变换后的图像实现反转:



  • 红色色值=255-原红色色值

  • 绿色色值=255-原绿色色值

  • 蓝色色值=255-原蓝色色值


那么构建如下矩阵就可以了。


反转色变换矩阵


由于最后一行数值对实际变化没影响,因此实际构建 ColorFilter.matrix 的时候,只需要传入20个参数就可以了。下面是应用了反转效果后的灭霸图,灭霸看起来像一个雕塑了。



下面我们先来看一下使用矩阵实现彩色图变灰度图,用下面的矩阵就能实现,最终得到变换后的 R、G、B值是相等的,而且三个色值的系数相加等于1(保证数值不会超出255)。这个矩阵是官方提供的,实际上也是经过图像学研究推导得到的。


灰度变换公式


对应灰度变换的 ColorFilter 的构造代码如下:


const greyScale = ColorFilter.matrix(<double>[
0.2126, 0.7152, 0.0722, 0, 0,
0.2126, 0.7152, 0.0722, 0, 0,
0.2126, 0.7152, 0.0722, 0, 0,
0, 0, 0, 1, 0,
]);

最后,我们来看看颜色循环变换的效果,颜色循环变换就是红色部分变为原先像素的绿色值,绿色部分变到原先像素的蓝色值,然后蓝色部分变到原先像素的红色值,对应的 ColorFilter 构造代码如下:


var colorRotation = ColorFilter.matrix(<double>[
0, 1, 0, 0, 0,
0, 0, 1, 0, 0,
1, 0, 0, 0, 0,
0, 0, 0, 1, 0
]);

有了这个我们其实就可以做一些动效了,比如我们把变化过程由动画值控制,得到下面的矩阵。


var colorRotation = ColorFilter.matrix(<double>[
animationValue, 1-animationValue, 0, 0, 0,
0, animationValue, 1-animationValue, 0, 0,
1-animationValue, 0, animationValue, 0, 0,
0, 0, 0, 1, 0
]);

我们看看灭霸图片颜色变化的动画效果,整个画面的色调在不断的变化,感觉像灭霸要开始“打响指”了。


颜色变化动画.gif


ColorFilter 的应用


ColorFilter 的最佳应用场景应该是图片滤镜,我们在图片类应用经常会看到各种滤镜效果(取得名字都很好听,比如什么“清纯”、“蓝调”,“怀旧”等等),实际上这种效果就是将一个颜色预置的变换矩阵应用到图片上。


总结


本篇介绍了颜色过滤器 ColorFilter 的应用以及原理,我们绘图的时候可以使用 ColorFilter 处理图片,实现类似滤镜的效果。如果考虑简单使用,也可以直接使用 ColorFiltered 组件。




本篇源码已上传至:绘图相关源码,文件名为:color_filter_demo.dart


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

Android抓包从未如此简单


·  阅读 407

一、情景再现:

有一天你正在王者团战里杀的热火朝天,忽然公司测试人员打你电话问为什么某个功能数据展示不出来了,昨天还好好的纳,是不是你又偷偷写bug了。。。WTF!,你会说你把手机给我,我连上电脑看看打印的请求日志是不是接口有问题,然后吭哧吭哧搞半天看到接口数据返回的格式确实不会,然后去群里丢了几句服务端人员看一下这个接口,数据有问题。然后有回去打游戏,可惜游戏早已结束,以失败告终,自己还没无情的举报禁赛了。。。人生最痛苦的事莫过于如此。假如你的项目已经继承了抓包助手,并且也给其他人员较少过如何使用,那么像这类问题根本就不需要你再来处理了,遇到数据问题他们第一时间会自己看请求数据,而你就可以安心上王者了。

二、Android抓包现状

目前常见的抓包工具有Charles、Fiddler、Wireshark等,这些或多或少都需要一些配置,略显麻烦,只适合开发及测试人员玩,如果产品也想看数据怎么办纳,别急,本文的主角登场了,你可以在项目中集成AndroidMonitor,只需两步简单配置即可实现抓包数据可视化功能,随时随地,人人都可以方便快捷的查看。

三、效果展示

俗话说无图无真相

111.jpg

222.jpg

333.jpg

抓包pc.png

四、如何使用

抓包工具有两个依赖需要添加:monito和monitor-plugin

Demo下载体验

1、monitor接入

添加依赖

   debugImplementation 'io.github.lygttpod:monitor:0.0.4'
复制代码

-备注: 使用debugImplementation是为了只在测试环境中引入

2、monitor-plugin接入

  1. 根目录build.gradle下添加如下依赖
    buildscript {
dependencies {
......
//monitor-plugin需要
classpath 'io.github.lygttpod:monitor-plugin:0.0.1'
}
}

复制代码

2.添加插件

    在APP的build.gradle中添加:

//插件内部会自动判断debug模式下hook到okhttp
apply plugin: 'monitor-plugin'

复制代码

原则上完成以上两步你的APP就成功集成了抓包工具,很简单有没有,如需定制化服务请看下边的个性化配置

3、 个性化配置

1、修改桌面抓包工具入口名字:在主项目string.xml中添加 monitor_app_name即可,例如:
```
<string name="monitor_app_name">XXX-抓包</string>
```
2、定制抓包入口logo图标:
```
添加 monitor_logo.png 即可
```
3、单个项目使用的话,添加依赖后可直接使用,无需初始化,库里会通过ContentProvider方式自动初始化

默认端口8080(端口号要唯一)

4、多个项目都集成抓包工具,需要对不同项目设置不同的端口和数据库名字,用来做区分

在主项目assets目录下新建 monitor.properties 文件,文件内如如下:对需要变更的参数修改即可
```
# 抓包助手参数配置
# Default port = 8080
# Default dbName = monitor_db
# ContentTypes白名单,默认application/json,application/xml,text/html,text/plain,text/xml
# Default whiteContentTypes = application/json,application/xml,text/html,text/plain,text/xml
# Host白名单,默认全部是白名单
# Default whiteHosts =
# Host黑名单,默认没有黑名单
# Default blackHosts =
# 如何多个项目都集成抓包工具,可以设置不同的端口进行访问
monitor.port=8080
monitor.dbName=app_name_monitor_db
```
复制代码

4、 proguard(默认已经添加混淆,如遇到问题可以添加如下混淆代码)

```
# monitor
-keep class com.lygttpod.monitor.** { *; }
```
复制代码

5、 温馨提示

    虽然monitor-plugin只会在debug环境hook代码,
但是release版编译的时候还是会走一遍Transform操作(空操作),
为了保险起见建议生产包禁掉此插件。

在jenkins打包机器的《生产环境》的local.properties中添加monitor.enablePlugin=false,全面禁用monitor插件
复制代码

6、如何使用

  • 集成之后编译运行项目即可在手机上自动生成一个抓包入口的图标,点击即可打开可视化页面查看网络请求数据,这样就可以随时随地的查看我们的请求数据了。
  • 虽然可以很方便的查看请求数据了但是手机屏幕太小,看起来不方便怎么办呐,那就去寻找在PC上展示的方法,首先想到的是能不能直接在浏览器里边直接看呐,这样不用安装任何程序在浏览输入一个地址就可以直接查看数据
  • PC和手机在同一局域网的前提下:直接在任意浏览器输入 手机ip地址+抓包工具设置的端口号即可(地址可以在抓包app首页TitleBar上可以看到)

7、关键原理说明

  • 拦截APP的OKHTTP请求(添加拦截器处理抓包请求,使用ASM字节码插装技术实现)
  • 数据保存到本地数据库(room)
  • APP本地开启一个socket服务AndroidLocalService
  • 与本地socket服务通信
  • UI展示数据(手机端和PC端)

浏览器检测之趣事

web
1 那段历史在开发过程中,我们通常用用户代理字符串—浏览器端 window.navigator.userAgent或者服务器端header携带的user-agent —来用于检测当前浏览器是否为移动端, 比如:if(isMobile()) { // 移动端逻...
继续阅读 »

1 那段历史

在开发过程中,我们通常用用户代理字符串—浏览器端 window.navigator.userAgent或者服务器端header携带的user-agent —来用于检测当前浏览器是否为移动端, 比如:

if(isMobile()) {
// 移动端逻辑...
}

function isMobile () {
  const versions = (function () {
      const u = window.navigator.userAgent // 服务器端:req.header('user-agent')
      return {
        trident: u.indexOf('Trident') > -1, // IE内核
        presto: u.indexOf('Presto') > -1, // opera内核
        webKit: u.indexOf('AppleWebKit') > -1, // 苹果、谷歌内核
        gecko: u.indexOf('Gecko') > -1 && u.indexOf('KHTML') === -1, // 火狐内核
        mobile: !!u.match(/AppleWebKit.*Mobile.*/), // 是否为移动终端
        ios: !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/), // ios终端
        android: u.indexOf('Android') > -1 || u.indexOf('Linux') > -1, // android终端或者uc浏览器
        iPhone: u.indexOf('iPhone') > -1, // 是否为iPhone或者QQHD浏览器
        iPad: u.indexOf('iPad') > -1, // 是否iPad
        webApp: u.indexOf('Safari') === -1
      }
  }())
  return versions.mobile || versions.ios || versions.android || versions.iPhone || versions.iPad
}

我在使用时心里一直有疑问,一个移动端,为什么要做那么多判断呢?

目前我的 Chrome 浏览器:


看到这么一长串字符串,我表示更懵逼, Mozilla不是firefox的厂商么?这是 Chrome 浏览器,又怎么会有 “Safari” 的关键字?那个 “like Gecko” 又是什么鬼?

于是抱着这些疑问, 我打算好好深入了解一下浏览器检测这部分,没想到在学习过程中发现了挺有意思的事情,待我慢慢道来,大家也听个乐呵。

首先始于客户端与服务器端通信,要求携带名称与版本信息,于是服务器端与客户端协定好在每个HTTP请求的头部加上用户代理字符串(userAgent),方便服务器端进行检测,检测通过之后再进行后续操作。

早期的用户代理字符串(userAgent)很简单, 就 "产品名称/产品版本号",比如:"Mosaic/0.9"。93年之后,网景公司发布的Netscape Navigator 系列浏览器渐渐成为了当时最受欢迎的浏览器,于是它拥有了规则制定权,说从此以后我的用户代理字符串就为:


这时肯定有人会问,"Mozilla" 是网景公司为 Netscape 浏览器定义的代号,既然站在“食物链”顶端,那当然得用自己的命名,这能理解。可为啥直到现在,大部分主流浏览器的用户代理字符串(userAgent),第一个名称也是 “Mozilla” 呢?

这就是我即将要讲的, 第一根搅屎棍——微软。

96年,微软推出了 IE3, 而当时 Netscape Navigator3 的市场占有率太高,微软说,为了兼容 Netscape Navigator3, IE的用户代理字符串从此就为:


看到没有, 第一个名称还是 “Mozilla”,这个误导信息可以直接骗过服务器检测,而真正的 IE 版本放到后面去了。

大概意思就是初出茅庐的IE小同学怕自己知名度太低,万一服务端检测不到自己,用户流失了怎么办?隔壁老大哥家大业大,那就干脆去蹭波流量吧。关键是蹭流量就蹭流量吧,还嘴硬说我这可是Mozilla/2.0哦,不是Mozilla/3.0哦,跟那个Netscape Navigator3 不能说没有关系,只能说毫不相干。于是,IE成功地将自己伪装成了 Netscape Navigator。

这在当时来说是有争议,但不得不说, 微软这波操作相当精准。精准到直到97年 IE4 发布时,IE 的市场份额大幅增加,有了点话语权,也不藏着掖着了, 就跟 Netscape 同时将版本升级到了 Mozilla/4.0, 之后就一直保持同步了。

看到 IE 这波操作,场外观众有点坐不住了,更多的浏览器厂商沿着IE的老路,蹭着 Netscape 的流量,在此基础上依葫芦画瓢地设定自己的用户代理字符串(userAgent)。直到 Gecko 渲染引擎 (firefox的核心) 开始大流行,用户代理字符串(userAgent)基本已经形成了一个比较标准格式,服务端检测也能识别到 “Mozilla”、“Geoko” 等关键字,与之前字符串相比, 还增加了引擎、语言信息等等。


接下来我要说第二根搅屎棍——苹果。

2003年,苹果发布了 Safari, 它说,我的浏览器用户代理字符串是这样的:


Safari 用的渲染引擎是WebKit, 不是Gecko,它的核心是在渲染引擎KHTML基础上进行开发的,但是当时大部分浏览器的用户代理字符串(userAgent)都包含了 “Mozilla”、“Gecko”等关键字供服务器端检测。

苹果昂着脸,维持着表面的高傲,表示我的 WebKit 天下无敌、傲视群雄, 心里却颤颤发抖,小心翼翼地在用户代理字符串里加了个“like Gecko”,假装我是Gecko ?!

这波操作可谓是又当又立的典范!

我想可能心理阴影最大的要属 Netscape 了,本来 IE 来白嫖一波也就算了,你Safari 也要来,而且本身苹果的影响力就不容小觑,你再进来插一脚,让我以后怎么生存?但苹果说:“Safari 与 Mozilla 兼容,不能让网站以为用户使用了不受支持的浏览器而把 Safari 排斥在外。”大概意思是,我就是要白嫖, 怎么样?可以说是相当不要脸了。

不过至少苹果还有点藏着掖着, 而 Chrome 就有点不讲武德,它说,成年人的世界不做选择, 我想要的我都要:


Chrome 的渲染引擎是 Blink , Javascript引擎是 V8, 但它的用户代理字符串(userAgent)中, 不仅包含了“Mozilla”、“like Gecko”,还包含了 “WebKit” 的引擎信息, 几乎把能嫖的都嫖了, 只多了一个 “Chrome” 名称和版本号,甚至都没有一个 “Blink” 关键字,节操碎了一地,简直触目惊心,令人叹为观止。

到这里就不得不提一嘴高冷的Opera,直到Opera 8,用户代理字符串(userAgent)一直都是 “Opera/Version (OS-or-CPU; Encryption [; language])”

Opera 一直给人一种世人皆醉我独清、出淤泥而不染的气概。到直到 Opera9 画风突然变了, 估计也是看到几个大厂商各种骚操作,有点绷不住了,也跑去蹭流量。心态虽然崩但高冷人设不能崩,我就是不走寻常路,于是秀了一波玄学操作,它搞了两套用户代理字符串(userAgent):


场外观众表示有点看不懂, 蹭完 Firefox 又去蹭 IE,还得分开蹭,这哪是秀操作, 这可是秀智商啊!纵观浏览器发展的这几十年,大概就是长江后浪推前浪,后浪还没把前浪踩死在沙滩上,后后浪又踩过来的一段历史吧。就在这历史的溪流中,用户代理字符串(userAgent)也已经形成了一个比较标准的格式。

目前,各个浏览器的用户代理字符串(userAgent)始终包含着以下信息:


至于后来移动端的 IOS 和 Andriod 基本的格式就成了:


这里的Mobile可能是 “iphone”、“ipad”、“BlackBerry”等等,Andriod设备的OS-or-CPU通常都是“Andriod”或“Linux”。所以,回到开头的isMobile检测函数内部,一大堆的检测判断条件, 简直就是一粒粒历史尘埃的堆叠。

同时,本地Chrome浏览器输出:


我也可以翻译一下,大概意思就是,白嫖的Mozilla/5.0 + Macintosh平台 + Mac OS操作系统 × 10_15_7版本白嫖的AppleWebKit引擎/537.36引擎版本号 (KHTML内核, like Gecko 假装我是Gecko) Chrome浏览器/浏览器版本号99.0.4844.84 白嫖的Safari/Sarari版本号537.36。

本人表示很精彩, 一个用户代理字符串犹如看了一场轰轰烈烈(巨不要脸)、你挣我夺(你蹭我蹭)的大戏!

2 第三方插件

接下来, 为懒人推荐几款用于浏览器检测的省事的第三方插件。

1、如果只是检测设备是否为手机端, 可以用 isMobile ,它支持在node端或浏览器端使用。

地址:https://github.com/kaimallea/isMobile

2、如果要检测设备的类型、版本、CPU等信息,可以用 UAParser ,它支持在node端或浏览器端使用。

地址:https://github.com/faisalman/ua-parser-js

3、vue插件,vue-browser-detect-plugin

地址:https://github.com/ICJIA/vue-browser-detect-plugin

4、react插件,react-device-detect

地址:https://github.com/duskload/react-device-detect

5、在不同平台,要在Html中设置对应平台的CSS,可以用 current-device

地址:https://github.com/matthewhudson/current-device

需要注意的是, 第三方插件虽好用, 但也要注意安全问题哦,之前 UAParser 就被曝出被遭遇恶意投毒,所以只是简单的检测尽量手写。

3 移动端与PC端分流

移动端与PC端分流,可以用 nginx 来操作, nginx 可以通过 $http_user_agent 直接拿到用户代理信息:

http { 
server {
    listen 80;
      server_name localhost;
      location / {
          root /usr/share/nginx/pc; #pc端代码目录
          if ($http_user_agent ~* '(Android|webOS|iPhone|iPod|BlackBerry)') {
          root /usr/share/nginx/mobile; #移动端代码目录
          }
      index index.html;
      }
}
}

来源:八戒技术团队

收起阅读 »

B站:2021.07.13 我们是这样崩的

至暗时刻2021年7月13日22:52,SRE收到大量服务和域名的接入层不可用报警,客服侧开始收到大量用户反馈B站无法使用,同时内部同学也反馈B站无法打开,甚至APP首页也无法打开。基于报警内容,SRE第一时间怀疑机房、网络、四层LB、七层SLB等基础设施出现...
继续阅读 »

至暗时刻

2021年7月13日22:52,SRE收到大量服务和域名的接入层不可用报警,客服侧开始收到大量用户反馈B站无法使用,同时内部同学也反馈B站无法打开,甚至APP首页也无法打开。基于报警内容,SRE第一时间怀疑机房、网络、四层LB、七层SLB等基础设施出现问题,紧急发起语音会议,拉各团队相关人员开始紧急处理(为了方便理解,下述事故处理过程做了部分简化)。

初因定位

22:55 远程在家的相关同学登陆VPN后,无法登陆内网鉴权系统(B站内部系统有统一鉴权,需要先获取登录态后才可登陆其他内部系统),导致无法打开内部系统,无法及时查看监控、日志来定位问题。

22:57 在公司Oncall的SRE同学(无需VPN和再次登录内网鉴权系统)发现在线业务主机房七层SLB(基于OpenResty构建) CPU 100%,无法处理用户请求,其他基础设施反馈未出问题,此时已确认是接入层七层SLB故障,排除SLB以下的业务层问题。

23:07 远程在家的同学紧急联系负责VPN和内网鉴权系统的同学后,了解可通过绿色通道登录到内网系统。

23:17 相关同学通过绿色通道陆续登录到内网系统,开始协助处理问题,此时处理事故的核心同学(七层SLB、四层LB、CDN)全部到位。

故障止损

23:20 SLB运维分析发现在故障时流量有突发,怀疑SLB因流量过载不可用。因主机房SLB承载全部在线业务,先Reload SLB未恢复后尝试拒绝用户流量冷重启SLB,冷重启后CPU依然100%,未恢复。

23:22 从用户反馈来看,多活机房服务也不可用。SLB运维分析发现多活机房SLB请求大量超时,但CPU未过载,准备重启多活机房SLB先尝试止损。

23:23 此时内部群里同学反馈主站服务已恢复,观察多活机房SLB监控,请求超时数量大大降低,业务成功率恢复到50%以上。此时做了多活的业务核心功能基本恢复正常,如APP推荐、APP播放、评论&弹幕拉取、动态、追番、影视等。非多活服务暂未恢复。

23:25 - 23:55 未恢复的业务暂无其他立即有效的止损预案,此时尝试恢复主机房的SLB。

  • 我们通过Perf发现SLB CPU热点集中在Lua函数上,怀疑跟最近上线的Lua代码有关,开始尝试回滚最近上线的Lua代码。

  • 近期SLB配合安全同学上线了自研Lua版本的WAF,怀疑CPU热点跟此有关,尝试去掉WAF后重启SLB,SLB未恢复。

  • SLB两周前优化了Nginx在balance_by_lua阶段的重试逻辑,避免请求重试时请求到上一次的不可用节点,此处有一个最多10次的循环逻辑,怀疑此处有性能热点,尝试回滚后重启SLB,未恢复。

  • SLB一周前上线灰度了对 HTTP2 协议的支持,尝试去掉 H2 协议相关的配置并重启SLB,未恢复。

新建源站SLB

00:00 SLB运维尝试回滚相关配置依旧无法恢复SLB后,决定重建一组全新的SLB集群,让CDN把故障业务公网流量调度过来,通过流量隔离观察业务能否恢复。

00:20 SLB新集群初始化完成,开始配置四层LB和公网IP。

01:00 SLB新集群初始化和测试全部完成,CDN开始切量。SLB运维继续排查CPU 100%的问题,切量由业务SRE同学协助。

01:18 直播业务流量切换到SLB新集群,直播业务恢复正常。

01:40 主站、电商、漫画、支付等核心业务陆续切换到SLB新集群,业务恢复。

01:50 此时在线业务基本全部恢复。

恢复SLB

01:00 SLB新集群搭建完成后,在给业务切量止损的同时,SLB运维开始继续分析CPU 100%的原因。

01:10 - 01:27 使用Lua 程序分析工具跑出一份详细的火焰图数据并加以分析,发现 CPU 热点明显集中在对 lua-resty-balancer 模块的调用中,从 SLB 流量入口逻辑一直分析到底层模块调用,发现该模块内有多个函数可能存在热点。

01:28 - 01:38 选择一台SLB节点,在可能存在热点的函数内添加 debug 日志,并重启观察这些热点函数的执行结果。

01:39 - 01:58 在分析 debug 日志后,发现 lua-resty-balancer模块中的 _gcd 函数在某次执行后返回了一个预期外的值:nan,同时发现了触发诱因的条件:某个容器IP的weight=0。

01:59 - 02:06 怀疑是该 _gcd 函数触发了 jit 编译器的某个 bug,运行出错陷入死循环导致SLB CPU 100%,临时解决方案:全局关闭 jit 编译。

02:07 SLB运维修改SLB 集群的配置,关闭 jit 编译并分批重启进程,SLB CPU 全部恢复正常,可正常处理请求。同时保留了一份异常现场下的进程core文件,留作后续分析使用。

02:31 - 03:50 SLB运维修改其他SLB集群的配置,临时关闭 jit 编译,规避风险。

根因定位

11:40 在线下环境成功复现出该 bug,同时发现SLB 即使关闭 jit 编译也仍然存在该问题。此时我们也进一步定位到此问题发生的诱因:在服务的某种特殊发布模式中,会出现容器实例权重为0的情况。

12:30 经过内部讨论,我们认为该问题并未彻底解决,SLB 仍然存在极大风险,为了避免问题的再次产生,最终决定:平台禁止此发布模式;SLB 先忽略注册中心返回的权重,强制指定权重。

13:24 发布平台禁止此发布模式。

14:06 SLB 修改Lua代码忽略注册中心返回的权重。

14:30 SLB 在UAT环境发版升级,并多次验证节点权重符合预期,此问题不再产生。

15:00 - 20:00 生产所有 SLB 集群逐渐灰度并全量升级完成。

原因说明

背景

B站在19年9月份从Tengine迁移到了OpenResty,基于其丰富的Lua能力开发了一个服务发现模块,从我们自研的注册中心同步服务注册信息到Nginx共享内存中,SLB在请求转发时,通过Lua从共享内存中选择节点处理请求,用到了OpenResty的lua-resty-balancer模块。到发生故障时已稳定运行快两年时间。

在故障发生的前两个月,有业务提出想通过服务在注册中心的权重变更来实现SLB的动态调权,从而实现更精细的灰度能力。SLB团队评估了此需求后认为可以支持,开发完成后灰度上线。

诱因

  • 在某种发布模式中,应用的实例权重会短暂的调整为0,此时注册中心返回给SLB的权重是字符串类型的"0"。此发布模式只有生产环境会用到,同时使用的频率极低,在SLB前期灰度过程中未触发此问题。

  • SLB 在balance_by_lua阶段,会将共享内存中保存的服务IP、Port、Weight 作为参数传给lua-resty-balancer模块用于选择upstream server,在节点 weight = "0" 时,balancer 模块中的 _gcd 函数收到的入参 b 可能为 "0"。

根因


  • Lua 是动态类型语言,常用习惯里变量不需要定义类型,只需要为变量赋值即可。

  • Lua在对一个数字字符串进行算术操作时,会尝试将这个数字字符串转成一个数字。

  • 在 Lua 语言中,如果执行数学运算 n % 0,则结果会变为 nan(Not A Number)。

  • _gcd函数对入参没有做类型校验,允许参数b传入:"0"。同时因为"0" != 0,所以此函数第一次执行后返回是 _gcd("0",nan)。如果传入的是int 0,则会触发[ if b == 0 ]分支逻辑判断,不会死循环。

  • _gcd("0",nan)函数再次执行时返回值是 _gcd(nan,nan),然后Nginx worker开始陷入死循环,进程 CPU 100%。

问题分析

\1. 为何故障刚发生时无法登陆内网后台?

事后复盘发现,用户在登录内网鉴权系统时,鉴权系统会跳转到多个域名下种登录的Cookie,其中一个域名是由故障的SLB代理的,受SLB故障影响当时此域名无法处理请求,导致用户登录失败。流程如下:


事后我们梳理了办公网系统的访问链路,跟用户链路隔离开,办公网链路不再依赖用户访问链路。

\2. 为何多活SLB在故障开始阶段也不可用?

多活SLB在故障时因CDN流量回源重试和用户重试,流量突增4倍以上,连接数突增100倍到1000W级别,导致这组SLB过载。后因流量下降和重启,逐渐恢复。此SLB集群日常晚高峰CPU使用率30%左右,剩余Buffer不足两倍。如果多活SLB容量充足,理论上可承载住突发流量, 多活业务可立即恢复正常。此处也可以看到,在发生机房级别故障时,多活是业务容灾止损最快的方案,这也是故障后我们重点投入治理的一个方向。


\3. 为何在回滚SLB变更无效后才选择新建源站切量,而不是并行?

我们的SLB团队规模较小,当时只有一位平台开发和一位组件运维。在出现故障时,虽有其他同学协助,但SLB组件的核心变更需要组件运维同学执行或review,所以无法并行。

\4. 为何新建源站切流耗时这么久?

我们的公网架构如下:


此处涉及三个团队:

  • SLB团队:选择SLB机器、SLB机器初始化、SLB配置初始化

  • 四层LB团队:SLB四层LB公网IP配置

  • CDN团队:CDN更新回源公网IP、CDN切量

SLB的预案中只演练过SLB机器初始化、配置初始化,但和四层LB公网IP配置、CDN之间的协作并没有做过全链路演练,元信息在平台之间也没有联动,比如四层LB的Real Server信息提供、公网运营商线路、CDN回源IP的更新等。所以一次完整的新建源站耗时非常久。在事故后这一块的联动和自动化也是我们的重点优化方向,目前一次新集群创建、初始化、四层LB公网IP配置已经能优化到5分钟以内。

\5. 后续根因定位后证明关闭jit编译并没有解决问题,那当晚故障的SLB是如何恢复的?

当晚已定位到诱因是某个容器IP的weight="0"。此应用在1:45时发布完成,weight="0"的诱因已消除。所以后续关闭jit虽然无效,但因为诱因消失,所以重启SLB后恢复正常。

如果当时诱因未消失,SLB关闭jit编译后未恢复,基于定位到的诱因信息:某个容器IP的weight=0,也能定位到此服务和其发布模式,快速定位根因。

优化改进

此事故不管是技术侧还是管理侧都有很多优化改进。此处我们只列举当时制定的技术侧核心优化改进方向。

1. 多活建设

在23:23时,做了多活的业务核心功能基本恢复正常,如APP推荐、APP播放、评论&弹幕拉取、动态、追番、影视等。故障时直播业务也做了多活,但当晚没及时恢复的原因是:直播移动端首页接口虽然实现了多活,但没配置多机房调度。导致在主机房SLB不可用时直播APP首页一直打不开,非常可惜。通过这次事故,我们发现了多活架构存在的一些严重问题:

多活基架能力不足

  • 机房与业务多活定位关系混乱。

  • CDN多机房流量调度不支持用户属性固定路由和分片。

  • 业务多活架构不支持写,写功能当时未恢复。

  • 部分存储组件多活同步和切换能力不足,无法实现多活。

业务多活元信息缺乏平台管理

  • 哪个业务做了多活?

  • 业务是什么类型的多活,同城双活还是异地单元化?

  • 业务哪些URL规则支持多活,目前多活流量调度策略是什么?

  • 上述信息当时只能用文档临时维护,没有平台统一管理和编排。

多活切量容灾能力薄弱

  • 多活切量依赖CDN同学执行,其他人员无权限,效率低。

  • 无切量管理平台,整个切量过程不可视。

  • 接入层、存储层切量分离,切量不可编排。

  • 无业务多活元信息,切量准确率和容灾效果差。

我们之前的多活切量经常是这么一个场景:业务A故障了,要切量到多活机房。SRE跟研发沟通后确认要切域名A+URL A,告知CDN运维。CDN运维切量后研发发现还有个URL没切,再重复一遍上面的流程,所以导致效率极低,容灾效果也很差。

所以我们多活建设的主要方向:

多活基架能力建设

  • 优化多活基础组件的支持能力,如数据层同步组件优化、接入层支持基于用户分片,让业务的多活接入成本更低。

  • 重新梳理各机房在多活架构下的定位,梳理Czone、Gzone、Rzone业务域。

  • 推动不支持多活的核心业务和已实现多活但架构不规范的业务改造优化。

多活管控能力提升

  • 统一管控所有多活业务的元信息、路由规则,联动其他平台,成为多活的元数据中心。

  • 支持多活接入层规则编排、数据层编排、预案编排、流量编排等,接入流程实现自动化和可视化。

  • 抽象多活切量能力,对接CDN、存储等组件,实现一键全链路切量,提升效率和准确率。

  • 支持多活切量时的前置能力预检,切量中风险巡检和核心指标的可观测。

2. SLB治理

架构治理

  • 故障前一个机房内一套SLB统一对外提供代理服务,导致故障域无法隔离。后续SLB需按业务部门拆分集群,核心业务部门独立SLB集群和公网IP。

  • 跟CDN团队、四层LB&网络团队一起讨论确定SLB集群和公网IP隔离的管理方案。

  • 明确SLB能力边界,非SLB必备能力,统一下沉到API Gateway,SLB组件和平台均不再支持,如动态权重的灰度能力。

运维能力

  • SLB管理平台实现Lua代码版本化管理,平台支持版本升级和快速回滚。

  • SLB节点的环境和配置初始化托管到平台,联动四层LB的API,在SLB平台上实现四层LB申请、公网IP申请、节点上线等操作,做到全流程初始化5分钟以内。

  • SLB作为核心服务中的核心,在目前没有弹性扩容的能力下,30%的使用率较高,需要扩容把CPU降低到15%左右。

  • 优化CDN回源超时时间,降低SLB在极端故障场景下连接数。同时对连接数做极限性能压测。

自研能力

  • 运维团队做项目有个弊端,开发完成自测没问题后就开始灰度上线,没有专业的测试团队介入。此组件太过核心,需要引入基础组件测试团队,对SLB输入参数做完整的异常测试。

  • 跟社区一起,Review使用到的OpenResty核心开源库源代码,消除其他风险。基于Lua已有特性和缺陷,提升我们Lua代码的鲁棒性,比如变量类型判断、强制转换等。

  • 招专业做LB的人。我们选择基于Lua开发是因为Lua简单易上手,社区有类似成功案例。团队并没有资深做Nginx组件开发的同学,也没有做C/C++开发的同学。

3. 故障演练

本次事故中,业务多活流量调度、新建源站速度、CDN切量速度&回源超时机制均不符合预期。所以后续要探索机房级别的故障演练方案:

  • 模拟CDN回源单机房故障,跟业务研发和测试一起,通过双端上的业务真实表现来验收多活业务的容灾效果,提前优化业务多活不符合预期的隐患。

  • 灰度特定用户流量到演练的CDN节点,在CDN节点模拟源站故障,观察CDN和源站的容灾效果。

  • 模拟单机房故障,通过多活管控平台,演练业务的多活切量止损预案。

4. 应急响应

B站一直没有NOC/技术支持团队,在出现紧急事故时,故障响应、故障通报、故障协同都是由负责故障处理的SRE同学来承担。如果是普通事故还好,如果是重大事故,信息同步根本来不及。所以事故的应急响应机制必须优化:

  • 优化故障响应制度,明确故障中故障指挥官、故障处理人的职责,分担故障处理人的压力。

  • 事故发生时,故障处理人第一时间找backup作为故障指挥官,负责故障通报和故障协同。在团队里强制执行,让大家养成习惯。

  • 建设易用的故障通告平台,负责故障摘要信息录入和故障中进展同步。

本次故障的诱因是某个服务使用了一种特殊的发布模式触发。我们的事件分析平台目前只提供了面向应用的事件查询能力,缺少面向用户、面向平台、面向组件的事件分析能力:

  • 跟监控团队协作,建设平台控制面事件上报能力,推动更多核心平台接入。

  • SLB建设面向底层引擎的数据面事件变更上报和查询能力,比如服务注册信息变更时某个应用的IP更新、weight变化事件可在平台查询。

  • 扩展事件查询分析能力,除面向应用外,建设面向不同用户、不同团队、不同平台的事件查询分析能力,协助快速定位故障诱因。

总结

此次事故发生时,B站挂了迅速登上全网热搜,作为技术人员,身上的压力可想而知。事故已经发生,我们能做的就是深刻反思,吸取教训,总结经验,砥砺前行。

此篇作为“713事故”系列之第一篇,向大家简要介绍了故障产生的诱因、根因、处理过程、优化改进。后续文章会详细介绍“713事故”后我们是如何执行优化落地的,敬请期待。

最后,想说一句:多活的高可用容灾架构确实生效了。

来源:哔哩哔哩技术

收起阅读 »

来了!解放你 Flutter Assets 的双手

以下是正文 Flutter 中加载本地资源最原始的方式是手动添加,然后硬编码路径,这种方式使用起来极其麻烦,也是我们开发者的痛点。这篇文章来介绍怎么用自动生成的方式来解放大家的双手,远离这个小痛点😉。 下面,我们来看怎么在 App 中使用资源,这些资源可以是...
继续阅读 »

以下是正文


Flutter 中加载本地资源最原始的方式是手动添加,然后硬编码路径,这种方式使用起来极其麻烦,也是我们开发者的痛点。这篇文章来介绍怎么用自动生成的方式来解放大家的双手,远离这个小痛点😉。


image.png


下面,我们来看怎么在 App 中使用资源,这些资源可以是图片,也可是字体。



· · ·

方式 1 : 手动添加


这是我们最原始的方式,也是带给我们痛苦的方式 😂,我们刚开始 Flutter 的时候基本就是这样的~


我们看一下这种方式麻烦在什么地方!怎么给我们自己制造麻烦的!


Step 1: 文件夹中添加图片


1_8MSLeRTWJJ9cNdRzWHymvg.png


Step 2: 添加图片到 pubspec.yaml 文件中


image.png


注意一点🤏:assets/ 会添加 assets/ 文件下所有可用的图片。


Step 3: 直接在代码中使用


import 'package:asset_generation/page2.dart';
import 'package:flutter/material.dart';

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

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Page 1'),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.next_plan),
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const Page2(),
),
),
),
body: Center(
child: Image.asset('assets/dash.png'),
),
);
}
}

我们再创建一个 Page2 页面,并且添加相同的代码。



import 'package:flutter/material.dart';

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

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Page 1'),
),
body: Center(
child: Image.asset('assets/dash.png'),
),
);
}
}

效果如下:


1_6nRtHc2RD8eU8i1VprJ_mA.gif


现在,假如我们想要修改文件的名字。只要我们改变了文件的名字,我们必须在代码中每一个使用到文件的地方修改一遍字符串。这就是痛苦且麻烦的地方!!!


在这里例子中,我们仅仅有两个页面,修改的时候貌似简单。但是我们维护的是一个大型 APP,开发者还修改了文件名,想想这个代码中重命名的任务就恶心🤢。



· · ·

方式 2 : 为资源变量创建一个常量文件


现在我们稍微进步一点点🤏来减缓我们的痛苦。我们创建一个常量来保持文件的路径,然后在代码中使用常量文件!


Step 1: 创建 constants.dart 文件


class Constants {
static String dashImage = 'assets/dash.png';
}

Step 2: 在Page1 和 Page2 中使用常量:


Center(
child: Image.asset(Constants.dashImage),
),

在这个例子里面,如果开发者想要修改文件名字,仅仅改变常量的内容就可以了,只在 Constants 类中一处而已。


Step 3: 自动创建常量文件


接下来就是魔法的地方~


Step 1: 在 pubspec.yaml 添加 flutter_gen 依赖


在 dependencies 下面添加 flutter_gen 依赖,然后在 dev_dependencies 添加 flutter_gen_runnerbuild_runner 依赖。


Step 2: 生成 assets


添加依赖之后,执行 flutter pub get,然后运行下面的命令:


flutter packages pub run build_runner build

这里命令之后,会创建一个 lib/gen 文件夹,在文件夹里面,会存在一个 assets.gen.dart 文件,这个文件会保存所有的资源信息!


Step 3: 在代码中使用


现在,使用生成的资源,开发者可以访问资源文件:


Center(
child: Image.asset(Assets.dash.path),
),

现在,加入开发者想要重命名文件,仅仅需要在运行一遍命令就可以了,我们什么也不用做了!



· · ·

希望大家喜欢文章~


如果文章对大家有帮助,并且想要在自己的 APP 中使用,可以在这个仓库中看 👉GitHub Repository


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

vue hash和history路由的区别

vue
在了解路由模式前,我们先看下 什么是单页面应用,vue-router  的实现原理是怎样的,这样更容易理解路由。 SPA与前端路由 SPA(单页面应用,全程为:Single-page Web applications)指的是只有一张Web页...
继续阅读 »


在了解路由模式前,我们先看下 什么是单页面应用,vue-router  的实现原理是怎样的,这样更容易理解路由。


SPA与前端路由


  • SPA(单页面应用,全程为:Single-page Web applications)指的是只有一张Web页面的应用,是加载单个HTML 页面并在用户与应用程序交互时动态更新该页面的Web应用程序,简单通俗点就是在一个项目中只有一个html页面,它在第一次加载页面时,将唯一完成的html页面和所有其余页面组件一起下载下来,所有的组件的展示与切换都在这唯一的页面中完成,这样切换页面时,不会重新加载整个页面,而是通过路由来实现不同组件之间的切换。
  • 单页面应用(SPA)的核心之一是:更新视图而不重新请求页面。

优点:


  • 具有桌面应用的即时性、网站的可移植性和可访问性
  • 用户体验好、快,内容的改变不需要重新加载整个页面
  • 良好的前后端分离,分工更明确

缺点:


  • 不利于搜索引擎的抓取
  • 首次渲染速度相对较慢

vue Router实现原理


vue-router  在实现单页面路由时,提供了两种方式:Hash  模式和  History  模式;vue2是 根据  mode  参数来决定采用哪种方式,默认是  Hash  模式,手动设置为  History  模式。更新视图但不重新请求页面”是前端路由原理的核心之一,目前在浏览器环境中这一功能的实现主要有以下两种方式:


image.png


Hash


简述


  • vue-router   默认为 hash 模式,使用 URL 的  hash  来模拟一个完整的 URL,当 URL 改变时,页面不会重新加载;#  就是  hash符号,中文名为哈希符或者锚点,在  hash  符号后的值称为  hash  值。
  • 路由的  hash  模式是利用了  window 可以监听 onhashchange 事件来实现的,也就是说  hash  值是用来指导浏览器动作的,对服务器没有影响,HTTP 请求中也不会包括  hash  值,同时每一次改变  hash  值,都会在浏览器的访问历史中增加一个记录,使用“后退”按钮,就可以回到上一个位置。所以,hash 模式 是根据  hash 值来发生改变,根据不同的值,渲染指定DOM位置的不同数据。

参考:Vue 前端路由工作原理,hash与history之间的区别


image.png


 特点


  • url中带一个   #   号
  • 可以改变URL,但不会触发页面重新加载(hash的改变会记录在  window.hisotry  中)因此并不算是一次 HTTP 请求,所以这种模式不利于 SEO 优化
  • 只能修改  #  后面的部分,因此只能跳转与当前 URL 同文档的 URL
  • 只能通过字符串改变 URL
  • 通过  window.onhashchange  监听  hash  的改变,借此实现无刷新跳转的功能。
  • 每改变一次  hash ( window.location.hash),都会在浏览器的访问历史中增加一个记录。
  • 路径中从  #  开始,后面的所有路径都叫做路由的  哈希值 并且哈希值它不会作为路径的一部分随着 http 请求,发给服务器

参考:在SPA项目的路由中,注意hash与history的区别


 History


简述


  • history  是路由的另一种模式,在相应的  router  配置时将  mode  设置为  history  即可。
  • history  模式是通过调用  window.history  对象上的一系列方法来实现页面的无刷新跳转。
  • 利用了 HTML5 History Interface  中新增的   pushState()  和  replaceState()  方法。
  • 这两个方法应用于浏览器的历史记录栈,在当前已有的  back、forward、go  的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 URL,但浏览器不会向后端发送请求。

 参考:深入了解前端路由 hash 与 history 差异



特点


  • 新的URL可以是与当前URL同源的任意 URL,也可以与当前URL一样,但是这样会把重复的一次操作记录到栈中。
  • 通过参数stateObject可以添加任意类型的数据到记录中。
  • 可额外设置title属性供后续使用。
  • 通过pushState、replaceState实现无刷新跳转的功能。
  • 路径直接拼接在端口号后面,后面的路径也会随着http请求发给服务器,因此前端的URL必须和向发送请求后端URL保持一致,否则会报404错误。
  • 由于History API的缘故,低版本浏览器有兼容行问题。

参考:在SPA项目的路由中,注意hash与history的区别前端框架路由实现的Hash和History两种模式的区别


 生产环境存在问题


       因为  history  模式的时候路径会随着  http 请求发送给服务器,项目打包部署时,需要后端配置 nginx,当应用通过  vue-router  跳转到某个页面后,因为此时是前端路由控制页面跳转,虽然url改变,但是页面只是内容改变,并没有重新请求,所以这套流程没有任何问题。但是,如果在当前的页面刷新一下,此时会重新发起请求,如果  nginx  没有匹配到当前url,就会出现404的页面。


那为什么hash模式不会出现这个问题呢?


     上文已讲,hash 虽然可以改变URL,但不会被包括在  HTTP  请求中。它被用来指导浏览器动作,并不影响服务器端,因此,改变  hash  并没有改变URL,所以页面路径还是之前的路径,nginx  不会拦截。 因此,切记在使用  history  模式时,需要服务端允许地址可访问,否则就会出现404的尴尬场景。


那为什么开发环境时就不会出现404呢?


因为在 vue-cli  中  webpack  帮我们做了处理


 


 解决问题


生产环境 刷新 404 的解决办法可以在 nginx  做代理转发,在  nginx 中配置按顺序检查参数中的资源是否存在,如果都没有找到,让   nginx  内部重定向到项目首页。



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

如何让 x == 1 && x == 2 && x == 3 等式成立

如何让 x == 1 && x == 2 && x == 3 等式成立 某次面试,面试官突然问道:“如何让 x 等于 1 且让 x 等于 2 且让 x 等于 3 的等式成立?” 话音刚落,笔者立马失去意识,双眼一黑,两腿一蹬,心...
继续阅读 »

如何让 x == 1 && x == 2 && x == 3 等式成立


某次面试,面试官突然问道:“如何让 x 等于 1 且让 x 等于 2 且让 x 等于 3 的等式成立?


话音刚落,笔者立马失去意识,双眼一黑,两腿一蹬,心里暗骂:什么玩意儿!



虽然当时没回答上来,但觉得这题非常有意思,便在这为大家分享下后续的解题思路:


宽松相等 == 和严格相等 === 都能用来判断两个值是否“相等”,首先,我们要明确上文提到的等于指的是哪一种,我们先看下二者的区别:


(1) 对于基础类型之间的比较,== 和 === 是有区别的:


(1.1) 不同类型间比较,== 比较“转化成同一类型后的值”看“值”是否相等,=== 如果类型不同,其结果就是不等

(1.2) 同类型比较,直接进行“值”比较,两者结果一样

(2) 对于引用类型之间的比较,== 和 === 是没有区别的,都进行“指针地址”比较 


(3) 基础类型与引用类型之间的比较,== 和 === 是有区别的:


(3.1) 对于 ==,将引用类型转化为基础类型,进行“值”比较

(3.2) 因为类型不同,=== 结果为 false

“== 允许在相等比较中进行强制类型转换,而 === 不允许。”


由此可见,上文提到的等于指的宽松相等 ==,题目变为 “x == 1 && x == 2 && x == 3”。


那多种数据类型之间的相等比较又有哪些呢?笔者查阅了相关资料,如下所示:


同类型数据之间的相等比较


如果 Type(x) 等于 Type(y) ES5 规范 11.9.3.1 这样定义:



  1. 如果 Type(x)Undefined,返回 true



  2. 如果 Type(x)Null,返回 true



  3. 如果 Type(x)Number ,则


    • 如果 xNaN,返回 false
    • 如果 yNaN,返回 false
    • 如果 xy 的数字值相同,返回 true
    • 如果 x+0y-0,返回 true
    • 如果 x-0y+0,返回 true


  4. 如果 Type(x)String,则如果 xy 是字符的序列完全相同(相同的长度和相同位置相同的字符),则返回 true。否则,返回 false



  5. 如果 Type(x)Boolean,则如果 xy 都为 true 或都为 false,则返回 true。否则,返回 false



  6. 如果 xy 指向同一对象,则返回 true。否则,返回 false



null 和 undefined 之间的相等比较


nullundefined 之间的 == 也涉及隐式强制类型转换。ES5 规范 11.9.3.2-3 这样定义:


  1. 如果 xnullyundefined,则结果为 true
  2. 如果 xundefinedynull,则结果为 true

在 == 中,nullundefined 相等(它们也与其自身相等),除此之外其他值都不和它们两个相等。


这也就是说, 在 == 中nullundefined 是一回事。


var a = null;
var b;
a == b; // true
a == null; // true
b == null; // true
a == false; // false
b == false; // false
a == ""; // false
b == ""; // false
a == 0; // false
b == 0; // false

字符串和数字之间的相等比较


ES5 规范 11.9.3.4-5 这样定义:


  1. 如果 Type(x) 是数字,Type(y) 是字符串,则返回 x == ToNumber(y) 的结果。
  2. 如果 Type(x) 是字符串,Type(y) 是数字,则返回 ToNumber(x) == y 的结果。

var a = 42;

var b = "42";

a === b; // false

a == b; // true

因为没有强制类型转换,所以 a === bfalse,42 和 "42" 不相等。


根据规范,"42" 应该被强制类型转换为数字以便进行相等比较。


其他类型和布尔类型之间的相等比较


ES5 规范 11.9.3.6-7 这样定义:


  1. 如果 Type(x) 是布尔类型,则返回 ToNumber(x) == y 的结果;
  2. 如果 Type(y) 是布尔类型,则返回 x == ToNumber(y) 的结果。

仔细分析例子,首先:


var x = true;

var y = "42";

x == y; // false

Type(x) 是布尔值,所以 ToNumber(x)true 强制类型转换为 1,变成 1 == "42",二者的类型仍然不同,"42" 根据规则被强制类型转换为 42,最后变成 1 == 42,结果为 false


对象和非对象之间的相等比较


关于对象(对象 / 函数 / 数组)和标量基本类型(字符串 / 数字 / 布尔值)之间的相等比较,ES5 规范 11.9.3.8-9 做如下规定:


  1. 如果 Type(x) 是字符串或数字,Type(y) 是对象,则返回 x == ToPrimitive(y) 的结果;
  2. 如果 Type(x) 是对象,Type(y) 是字符串或数字,则返回 ToPromitive(x) == y 的结果。

什么是 toPrimitive() 函数?


**应用场景:**在 JavaScript 中,如果想要将对象转换成基本类型时,再从基本类型转换为对应的 String 或者 Number,实质就是调用 valueOftoString 方法,也就是所谓的拆箱转换。


**函数结构:**toPrimitive(input, preferedType?)


参数解释:


input 是输入的值,即要转换的对象,必选;


preferedType 是期望转换的基本类型,他可以是字符串,也可以是数字。选填,默认为 number


执行过程:


如果转换的类型是 number,会执行以下步骤:


  1. 如果 input 是原始值,直接返回这个值;
  2. 否则,如果 input 是对象,调用 input.valueOf(),如果结果是原始值,返回结果;
  3. 否则,调用input.toString()。如果结果是原始值,返回结果;
  4. 否则,抛出错误。

如果转换的类型是 string,2和3会交换执行,即先执行 toString() 方法。


valueOf 和 toString 的优先级:


  1. 进行对象转换时 (alert(对象)),优先调用 toString 方法,如没有重写 toString 将调用 valueOf 方法,如果两方法都不没有重写,但按 ObjecttoString 输出。
  2. 进行强转字符串类型时将优先调用 toString 方法,强转为数字时优先调用 valueOf
  3. 在有运算操作符的情况下,valueOf 的优先级高于 toString

由此可知,若 x 为对象时,我们改写 x 的 valueOf 或 toString 方法可以让标题的等式成立:


const x = {
val: 0,
valueOf: () => {
x.val++
return x.val
},
}

或者:


const x = {
val: 0,
toString: () => {
x.val++
return x.val
},
}

给对象 x 设置一个属性 val 并赋值为 0,并修改其 valueOf、toString 方法,在 “x == 1 && x == 2 && x == 3”判断执行时,每次等式比较都会触发 valueOf、toString 方法,都会执行 val++ ,同时把最新的 val 值用于等式比较,三次等式判断时 val 值分别为 1、2、3 与等式右侧的 1、2、3 相同,从而使等式成立。



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

大家好啊,世界您好啊,请多关照哈

大家好啊,世界您好啊,请多关照哈,,,,,,,,,,,

大家好啊,世界您好啊,请多关照哈,,,,,,,,,,,

Flutter: 卡顿检测,实用小工具推荐

前言 对于任何一款应用来说,页面的流畅度是用户体验最重要的几个指标之一。我们需要用数据的形式标识出页面的流畅程度。 如何了解页面流畅度 对于大部分人而言,当每秒的画面达到60,也就是俗称60FPS的时候,整个过程就是流畅的。一秒 60 帧,也就意味着平均两帧之...
继续阅读 »

前言


对于任何一款应用来说,页面的流畅度是用户体验最重要的几个指标之一。我们需要用数据的形式标识出页面的流畅程度。


如何了解页面流畅度


对于大部分人而言,当每秒的画面达到60,也就是俗称60FPS的时候,整个过程就是流畅的。一秒 60 帧,也就意味着平均两帧之间的间隔为 16.7ms。但并不意味着一秒低于60帧,人眼就会感觉到卡顿。小轰将查阅到的资料列出如下:



  • 流畅:FPS大于55,即一帧耗时低于 18ms

  • 良好:FPS在30-55之间,即一帧耗时在 18ms-33ms 之间

  • 轻微卡顿:FPS在15-30之间,即一帧耗时在 33ms-67ms 之间

  • 卡顿:FPS低于15,即一帧耗时大于 66.7ms


两款帧率检测工具


1. PerformanceOverLay


官方SDK为开发者提供的帧率检测工具,使用非常简单,在MaterialApp下添加属性showPerformanceOverlay:true


MaterialApp(
showPerformanceOverlay: true,
home: ...,
)

image.png
如图,PerformanceOverLay 会分别为我们展示了构建(UI)耗时和渲染(Raster)耗时。



注意:我们在判断流畅度的时候,要看一帧的总耗时(UI耗时+Raster耗时)。



2. fps_monitor


一款pub上的开源工具,链接地址:fps_monitor


集成步骤



  1. 添加引用 fps_monitor: ^2.0.0

  2. 根布局添加包裹组件


Widget build(BuildContext context) {
GlobalKey<NavigatorState> globalKey = GlobalKey();
WidgetsBinding.instance.addPostFrameCallback((t) {
//overlayState 为 fps_monitor 内提供变量,用于overlay.insert
overlayState = globalKey.currentState.overlay;
});
return MaterialApp(
showPerformanceOverlay: false,
navigatorKey: globalKey,
builder: (ctx, child) => CustomWidgetInspector(
child: child,
),
home: MyApp(),
);
}
复制代码

参数说明


  • navigatorKey : MaterialApp指定GlobalKey

  • overlayState 赋值: 指定overLayState ,因为需要弹出一个Fps的统计页面,所以当前指定overLayState。

  • CustomWidgetInspector: 在build属性中包裹组件


image.png



与 PerformanceOverLay 不同,fps_monitor在使用上更加直观,省略了两组数据的相加。



原理分析:



  • Flutter 会在每帧完成绘制后,将耗时进行回调List<FrameTiming> 。[构建时间;绘制时间;总时间]。WidgetsBinding.instance.addTimingsCallback(Function(List<FrameTiming> timings));

  • 每一帧的耗时 duration = frameTiming.totalSpan.inMilliseconds.toDouble()

  • 根据每一帧的耗时,依照规则进行流畅度匹配,完成widget的绘制。然后通过 overlay.insert(),作为浮窗展示给开发者

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

Flutter之GetX依赖注入使用详解

put 为了验证依赖注入的功能,首先创建两个测试页面:PageA 和 PageB ,PageA 添加两个按钮 toB 和 find ,分别为跳转 PageB 和获取依赖;在 PageB 中通过 put 方法注入依赖对象,然后调用按钮触发 find 获取依赖。关...
继续阅读 »

put


为了验证依赖注入的功能,首先创建两个测试页面:PageA 和 PageB ,PageA 添加两个按钮 toBfind ,分别为跳转 PageB 和获取依赖;在 PageB 中通过 put 方法注入依赖对象,然后调用按钮触发 find 获取依赖。关键源码如下:


PageA


TextButton(
child: const Text("toB"),
onPressed: (){
/// Navigator.push(context, MaterialPageRoute(builder: (context) => const PageB()));
/// Get.to(const PageB());
},
),

TextButton(
child: const Text("find"),
onPressed: () async {
User user = Get.find();
print("page a username : ${user.name} id: ${user.id}");
})

PageB:


Get.put(User.create("张三", DateTime.now().millisecondsSinceEpoch)));

User user = Get.find();

TextButton(
child: const Text("find"),
onPressed: (){
User user = Get.find();
print("${DateTime.now()} page b username : ${user.name} id: ${user.id}");
})

其中 User 为自定义对象,用于测试注入,源码如下:


User:


class User{
final String? name;
final int? id;

factory User.create(String name, int id){
print("${DateTime.now()} create User");
return User(name, id);
}
}

Navigator 路由跳转


首先使用 Flutter 自带的路由管理从 PageA 跳转 PageB, 然后返回 PageA 再点击 find 按钮获取 User 依赖:


Navigator.push(context, MaterialPageRoute(builder: (context) => const PageB()));

流程:PageA -> PageB -> put -> find -> PageA -> find


输出结果:


/// put
I/flutter (31878): 2022-01-27 19:18:20.851800 create User
[GETX] Instance "User" has been created

/// page b find
I/flutter (31878): 2022-01-27 19:18:22.170133 page b username : 张三 id: 1643282300139

/// page a find
I/flutter (31878): 2022-01-27 19:18:25.554667 page a username : 张三 id: 1643282300139

通过输出结果发现,在 PageB 注入的依赖 User,在返回 PageA 后通过 find 依然能获取,并且是同一个对象。通过 Flutter 通过源码一步一步剖析 Getx 依赖管理的实现 这篇文章知道,在页面销毁的时候会回收依赖,但是这里为什么返回 PageA 后还能获取到依赖对象呢?是因为在页面销毁时回收有个前提是使用 GetX 的路由管理页面,使用官方的 Navigator 进行路由跳转时页面销毁不会触发回收依赖。


GetX 路由跳转


接下来换成使用 GetX 进行路由跳转进行同样的操作,再看看输出结果:


Get.to(const PageB());

流程:PageA -> PageB -> put -> find -> PageA -> find


输出结果:


[GETX] GOING TO ROUTE /PageB

/// put
I/flutter (31878): 2022-01-27 19:16:32.014530 create User
[GETX] Instance "User" has been created

/// page b find
I/flutter (31878): 2022-01-27 19:16:34.043144 page b username : 张三 id: 1643282192014
[GETX] CLOSE TO ROUTE /PageB
[GETX] "User" deleted from memory

/// page a find error
E/flutter (31878): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: "User" not found. You need to call "Get.put(User())" or "Get.lazyPut(()=>User())"

发现在 PageB 中获取是正常,关闭 PageB 时输出了一句 "User" deleted from memory 即在 PageB 注入的 User 被删除了,此时在 PageA 再通过 find 获取 User 就报错了,提示需要先调用 put 或者 lazyPut 先注入依赖对象。这就验证了使用 GetX 路由跳转时,使用 put 默认注入依赖时,当页面销毁依赖也会被回收。


permanent


put 还有一个 permanent 参数,在 Flutter应用框架搭建(一)GetX集成及使用详解 这篇文章里介绍过,permanent 的作用是永久保留,默认为 false,接下来在 put 时设置 permanent 为 true,并同样使用 GetX 的路由跳转重复上面的流程。


关键代码:


Get.put(User.create("张三", DateTime.now().millisecondsSinceEpoch), permanent: true);

流程:PageA -> PageB -> put -> find -> PageA -> find


输出结果:


[GETX] GOING TO ROUTE /PageB
/// put
I/flutter (31878): 2022-01-27 19:15:16.110403 create User
[GETX] Instance "User" has been created

/// page b find
I/flutter (31878): 2022-01-27 19:15:18.667360 page b username : 张三 id: 1643282116109
[GETX] CLOSE TO ROUTE /PageB
[GETX] "User" has been marked as permanent, SmartManagement is not authorized to delete it.

/// page a find success
I/flutter (31878): page a username : 张三 id: 1643282116109

设置 permanent 为 true 后,返回 PageA 同样能获取到依赖对象,说明依赖并没有因为页面销毁而回收,GetX 的日志输出也说明了 User 被标记为 permanent 而不会被删除:"User" has been marked as permanent, SmartManagement is not authorized to delete it.


lazyPut


lazyPut 为延迟初始化依赖对象 :


Get.lazyPut(() => User.create("张三", DateTime.now().millisecondsSinceEpoch));

TextButton(
child: const Text("find"),
onPressed: () async {
User user = Get.find();
print("${DateTime.now()} page b username : ${user.name} id: ${user.id}");
})

流程:PageA -> PageB -> put -> find -> find -> PageA -> find, 从 PageA 跳转 PageB,先通过 lazyPut 注入依赖,然后点击 find 获取依赖,过 3 秒再点击一次,然后返回 PageA 点击 find 获取一次。


输出结果:


[GETX] GOING TO ROUTE /PageB
/// lazyPut

/// page b find 1
I/flutter (31878): 2022-01-27 17:38:49.590295 create User
[GETX] Instance "User" has been created
I/flutter (31878): 2022-01-27 17:38:49.603063 page b username : 张三 id: 1643276329589

/// page b find 2
I/flutter (31878): 2022-01-27 17:38:52.297049 page b username : 张三 id: 1643276329589
[GETX] CLOSE TO ROUTE /PageB
[GETX] "User" deleted from memory

/// page a find error
E/flutter (31878): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: "User" not found. You need to call "Get.put(User())" or "Get.lazyPut(()=>User())"

通过日志发现 User 对象是在第一次调用 find 时进行初始化话的,第二次 find 时不会再次初始化 User;同样的 PageB 销毁时依赖也会被回收,导致在 PageA 中获取会报错。


fenix


lazyPut 还有一个 fenix 参数默认为 false,作用是当销毁时,会将依赖移除,但是下次 find 时又会重新创建依赖对象。


lazyPut 添加 fenix 参数 :


 Get.lazyPut(() => User.create("张三", DateTime.now().millisecondsSinceEpoch), fenix: true);

流程:PageA -> PageB -> put -> find -> find -> PageA -> find,与上面流程一致。


输出结果:


[GETX] GOING TO ROUTE /PageB
/// lazyPut

/// page b find 1
[GETX] Instance "User" has been created
I/flutter (31878): 2022-01-27 17:58:58.321564 create User
I/flutter (31878): 2022-01-27 17:58:58.333369 page b username : 张三 id: 1643277538321

/// page b find 2
I/flutter (31878): 2022-01-27 17:59:01.647629 page b username : 张三 id: 1643277538321
[GETX] CLOSE TO ROUTE /PageB

/// page a find success
I/flutter (31878): 2022-01-27 17:59:07.666929 create User
[GETX] Instance "User" has been created
I/flutter (31878): page a username : 张三 id: 1643277547666

通过输出日志分析,在 PageB 中的表现与不加 fenix 表现一致,但是返回 PageA 后获取依赖并没有报错,而是重新创建了依赖对象。这就是 fenix 的作用。


putAsync


putAsyncput 基本一致,不同的是传入依赖可以异步初始化。测试代码修改如下:


print("${DateTime.now()} : page b putAsync User");
Get.putAsync(() async {
await Future.delayed(const Duration(seconds: 3));
return User.create("张三", DateTime.now().millisecondsSinceEpoch);
});

使用 Future.delayed 模拟耗时操作。


流程:PageA -> PageB -> put -> find -> PageA -> find


输出结果:


[GETX] GOING TO ROUTE /PageB

/// putAsync
I/flutter (31878): 2022-01-27 18:48:34.280337 : page b putAsync User

/// create user
I/flutter (31878): 2022-01-27 18:48:37.306073 create User
[GETX] Instance "User" has been created

/// page b find
I/flutter (31878): 2022-01-27 18:48:40.264854 page b username : 张三 id: 1643280517305
[GETX] CLOSE TO ROUTE /PageB
[GETX] "User" deleted from memory

/// page a find error
E/flutter (31878): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: "User" not found. You need to call "Get.put(User())" or "Get.lazyPut(()=>User())"

通过日志发现,put 后确实是过了 3s 才创建 User。


create


createpermanent 参数默认为 true,即永久保留,但是通过 Flutter应用框架搭建(一)GetX集成及使用详解 这篇源码分析知道,create 内部调用时 isSingleton 设置为 false,即每次 find 时都会重新创建依赖对象。


Get.create(() => User.create("张三", DateTime.now().millisecondsSinceEpoch));

流程:PageA -> PageB -> put -> find -> find -> PageA -> find


输出结果:


[GETX] GOING TO ROUTE /PageB
/// create

/// page b find 1
I/flutter (31878): 2022-01-27 18:56:10.520961 create User
I/flutter (31878): 2022-01-27 18:56:10.532465 page b username : 张三 id: 1643280970520

/// page b find 2
I/flutter (31878): 2022-01-27 18:56:18.933750 create User
I/flutter (31878): 2022-01-27 18:56:18.934188 page b username : 张三 id: 1643280978933

[GETX] CLOSE TO ROUTE /PageB

/// page a find success
I/flutter (31878): 2022-01-27 18:56:25.319224 create User
I/flutter (31878): page a username : 张三 id: 1643280985319

通过日志发现,确实是每次 find 时都会重新创建 User 对象,并且退出 PageB 后还能通过 find 获取依赖对象。


总结


通过代码调用不同的注入方法,设置不同的参数,分析输出日志,详细的介绍了 putlazyPutputAsynccreate 以及 permanentfenix 参数的具体作用,开发中可根据实际业务场景灵活使用不同注入方式。关于注入的 tag 参数将在后续文章中详细介绍。


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

原生Android工程接入Flutter aar

一、环境搭建 首先,需要开发者按照原生Android、iOS的搭建流程搭建好开发环境。然后,去Flutter官网下载最新的SDK,下载完毕后解压到自定义目录即可。如果出现下载问题,可以使用Flutter官方为中国开发者搭建的临时镜像。 export PUB_H...
继续阅读 »

一、环境搭建


首先,需要开发者按照原生Android、iOS的搭建流程搭建好开发环境。然后,去Flutter官网下载最新的SDK,下载完毕后解压到自定义目录即可。如果出现下载问题,可以使用Flutter官方为中国开发者搭建的临时镜像。


export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn

为了方便使用命令行,还需要额外配置下环境变量。首先,使用vim命令打开终端。


vim ~/.bash_profile  

然后,将如下代码添加到.bash_profile文件中,并使用source ~/.bash_profile命令使文件更改生效。


export PATH=/Users/mac/Flutter/flutter/bin:$PATH
//刷新.bash_profile
source ~/.bash_profile

完成上述操作之后,接下来使用flutter doctor命令检查环境是否正确,成功会输出如下信息。
在这里插入图片描述


二、创建Flutter aar包


原生Android集成Flutter主要有两种方式,一种是创建flutter module,然后以原生module那样依赖;另一种方式是将flutter module打包成aar,然后在原生工程中依赖aar包,官方推荐aar的方式接入。


创建flutter aar有两种方式,一种是使用Android Studio进行生成,另一种是直接使用命令行。使用命令行创建flutter module如下:


flutter create -t module flutter_module

然后,进入到flutter_module,执行flutter build aar命令生成aar包,如果没有任何出错,会在/flutter_module/.android/Flutter/build/outputs目录下生成对应的aar包,如下图。


在这里插入图片描述


build/host/outputs/repo
└── com
└── example
└── my_flutter
├── flutter_release
│ ├── 1.0
│ │ ├── flutter_release-1.0.aar
│ │ ├── flutter_release-1.0.aar.md5
│ │ ├── flutter_release-1.0.aar.sha1
│ │ ├── flutter_release-1.0.pom
│ │ ├── flutter_release-1.0.pom.md5
│ │ └── flutter_release-1.0.pom.sha1
│ ├── maven-metadata.xml
│ ├── maven-metadata.xml.md5
│ └── maven-metadata.xml.sha1
├── flutter_profile
│ ├── ...
└── flutter_debug
└── ...


当然,我们也可以使用Android Studio来生成aar包。依次选择File -> New -> New Flutter Project -> Flutter Module生成Flutter module工程。
在这里插入图片描述


然后我们依次选择build ->Flutter ->Build AAR即可生成aar包。


在这里插入图片描述
接下来,就是在原生Android工程中集成aar即可。


三、添加Flutter依赖


3.1 添加aar依赖


官方推荐方式


集成aar包的方式和集成普通的aar包的方式是一样大的。首先,在app的目录下新建libs文件夹 并在build.gradle中添加如下配置。


android {
...

buildTypes {
profile {
initWith debug
}
}

String storageUrl = System.env.FLUTTER_STORAGE_BASE_URL ?:
"https://storage.googleapis.com"
repositories {
maven {
url '/Users/mac/Flutter/module_flutter/build/host/outputs/repo'
}
maven {
url "$storageUrl/download.flutter.io"
}
}

}

dependencies {
debugImplementation 'com.xzh.module_flutter:flutter_debug:1.0'
profileImplementation 'com.xzh.module_flutter:flutter_profile:1.0'
releaseImplementation 'com.xzh.module_flutter:flutter_release:1.0'
}

本地Libs方式


当然,我们也可以把生成的aar包拷贝到本地libs中,然后打开app/build.grade添加本地依赖,如下所示。


repositories {
flatDir {
dirs 'libs'
}
}

dependencies {
...
//添加本地依赖
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation(name: 'flutter_debug-1.0', ext: 'aar')
implementation 'io.flutter:flutter_embedding_debug:1.0.0-f0826da7ef2d301eb8f4ead91aaf026aa2b52881'
implementation 'io.flutter:armeabi_v7a_debug:1.0.0-f0826da7ef2d301eb8f4ead91aaf026aa2b52881'
implementation 'io.flutter:arm64_v8a_debug:1.0.0-f0826da7ef2d301eb8f4ead91aaf026aa2b52881'
implementation 'io.flutter:x86_64_debug:1.0.0-f0826da7ef2d301eb8f4ead91aaf026aa2b52881'
}


io.flutter:flutter_embedding_debug来自哪里呢,其实是build/host/outputs/repo生成的时候flutter_release-1.0.pom文件中,
在这里插入图片描述


  <groupId>com.example.flutter_library</groupId>
<artifactId>flutter_release</artifactId>
<version>1.0</version>
<packaging>aar</packaging>
<dependencies>
<dependency>
<groupId>io.flutter.plugins.sharedpreferences</groupId>
<artifactId>shared_preferences_release</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.flutter</groupId>
<artifactId>flutter_embedding_release</artifactId>
<version>1.0.0-626244a72c5d53cc6d00c840987f9059faed511a</version>
<scope>compile</scope>
</dependency>

在拷贝的时候,注意我们本地aar包的环境,它们是一一对应的。接下来,为了能够正确依赖,还需要在外层的build.gradle中添加如下依赖。


buildscript {
repositories {
google()
jcenter()
maven {
url "http://download.flutter.io" //flutter依赖
}
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.0'
}
}

如果,原生Android工程使用的是组件化开发思路,通常是在某个module/lib下依赖,比如module_flutter进行添加。


 在module_flutter build.gradle下配置
repositories {
flatDir {
dirs 'libs' // aar目录
}
}

在主App 下配置
repositories {
// 详细路径
flatDir {
dirs 'libs', '../module_flutter/libs'
}
}

3.2 源码依赖


除了使用aar方式外, 我们还可以使用flutter模块源码的方式进行依赖。首先,我们在原生Android工程中创建一个module,如下图。
在这里插入图片描述
添加成功后,系统会默认在settings.gradle文件中生成如下代码。


 
include ':app'
setBinding(new Binding([gradle: this]))
evaluate(new File(
settingsDir.parentFile,
'my_flutter/.android/include_flutter.groovy'
))

然后,在app/build.gradle文件中添加源码依赖。


dependencies {
implementation project(':flutter')
}

3.3 使用 fat-aar 编译 aar


如果flutter 中引入了第三方的一些库,那么多个项目在使用flutter的时候就需要使用 fat-aar。首先,在 .android/build.gradle 中添加fat-aar 依赖。


 dependencies {
...
com.github.kezong:fat-aar:1.3.6
}


然后,在 .android/Flutter/build.gradle 中添加如下 plugin 和依赖。


dependencies {
testImplementation 'junit:junit:4.12'

// 添加 flutter_embedding.jar debug
embed "io.flutter:flutter_embedding_debug:1.0.0-eed171ff3538aa44f061f3768eec3a5908e8e852"
// 添加 flutter_embedding.jar release
embed "io.flutter:flutter_embedding_release:1.0.0-e1e6ced81d029258d449bdec2ba3cddca9c2ca0c"
// 添加各个 cpu 版本 flutter.so
embed "io.flutter:arm64_v8a_debug:1.0.0-eed171ff3538aa44f061f3768eec3a5908e8e852"
embed "io.flutter:armeabi_v7a_debug:1.0.0-eed171ff3538aa44f061f3768eec3a5908e8e852"
embed "io.flutter:x86_64_debug:1.0.0-eed171ff3538aa44f061f3768eec3a5908e8e852"
embed "io.flutter:x86_debug:1.0.0-eed171ff3538aa44f061f3768eec3a5908e8e852"

此时,如果我们运行项目,可能会报一个Cannot fit requested classes in a single dex file的错误。这是一个很古老的分包问题,意思是dex超过65k方法一个dex已经装不下了需要个多个dex。解决的方法是,只需要在 app/build.gradle 添加multidex即可。


android {
defaultConfig {
···
multiDexEnabled true
}
}

dependencies {
//androidx支持库的multidex库
implementation 'androidx.multidex:multidex:2.0.1'
}

五、跳转Flutter


5.1 启动FlutterActivity


集成Flutter之后,接下来我们在AndroidManifest.xml中注册FlutterActivity实现一个简单的跳转。


<activity
android:name="io.flutter.embedding.android.FlutterActivity"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
android:exported="true" />

然后在任何页面添加一个跳转代码,比如。


myButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startActivity(
FlutterActivity.createDefaultIntent(this)
);
}
});

不过当我运行项目,执行跳转的时候还是报错了,错误的信息如下。


   java.lang.RuntimeException: Unable to start activity ComponentInfo{com.snbc.honey_app/io.flutter.embedding.android.FlutterActivity}: java.lang.IllegalStateException: ensureInitializationComplete must be called after startInitialization
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2946)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3081)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1831)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:201)
at android.app.ActivityThread.main(ActivityThread.java:6806)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:873)

看报错应该是初始化的问题,但是官方文档没有提到任何初始化步骤相关的代码,查查Flutter 官方的issue,表示要加一行初始化代码:


public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
FlutterMain.startInitialization(this);
}
}

然后,我再次运行,发现报了如下错误。


java.lang.NoClassDefFoundError: Failed resolution of: Landroid/arch/lifecycle/DefaultLifecycleObserver;
at io.flutter.embedding.engine.FlutterEngine.<init>(FlutterEngine.java:152)
at io.flutter.embedding.android.FlutterActivityAndFragmentDelegate.setupFlutterEngine(FlutterActivityAndFragmentDelegate.java:221)
at io.flutter.embedding.android.FlutterActivityAndFragmentDelegate.onAttach(FlutterActivityAndFragmentDelegate.java:145)
at io.flutter.embedding.android.FlutterActivity.onCreate(FlutterActivity.java:399)
at android.app.Activity.performCreate(Activity.java:7224)
at android.app.Activity.performCreate(Activity.java:7213)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1272)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2926)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3081)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1831)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:201)
at android.app.ActivityThread.main(ActivityThread.java:6806)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:873)
Caused by: java.lang.ClassNotFoundException: Didn't find class "android.arch.lifecycle.DefaultLifecycleObserver" on path: DexPathList[[zip file "/data/app/com.example.myapplication-kZH0dnJ-qI1ow1NqGOB2ug==/base.apk"],nativeLibraryDirectories=[/data/app/com.example.myapplication-kZH0dnJ-qI1ow1NqGOB2ug==/lib/arm64, /data/app/com.example.myapplication-kZH0dnJ-qI1ow1NqGOB2ug==/base.apk!/lib/arm64-v8a, /system/lib64, /vendor/lib64]]

最后的日志给出的提示是lifecycle缺失,所以添加lifecycle的依赖即可,如下。


   implementation 'android.arch.lifecycle:common-java8:1.1.0'

然后再次运行就没啥问题了。
在这里插入图片描述


5.2 使用FlutterEngine启动


默认情况下,每个FlutterActivity被创建时都会创建一个FlutterEngine,每个FlutterEngine都有一个初始化操作。这意味着在启动一个标准的FlutterActivity时会有一定的延迟。为了减少此延迟,我们可以在启动FlutterActivity之前预先创建一个FlutterEngine,然后在跳转FlutterActivity时使用FlutterEngine即可。最常见的做法是在Application中先初始化FlutterEngine,比如。


class MyApplication : Application() {

lateinit var flutterEngine : FlutterEngine

override fun onCreate() {
super.onCreate()
flutterEngine = FlutterEngine(this)
flutterEngine.dartExecutor.executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
)
FlutterEngineCache
.getInstance()
.put("my_engine_id", flutterEngine)
}
}

然后,我们在跳转FlutterActivity时使用这个缓冲的FlutterEngine即可,由于FlutterEngine初始化的时候已经添加了engine_id,所以启动的时候需要使用这个engine_id进行启动。


myButton.setOnClickListener {
startActivity(
FlutterActivity
.withCachedEngine("my_engine_id")
.build(this)
)
}

当然,在启动的时候,我们也可以跳转一个默认的路由,只需要在启动的时候调用setInitialRoute方法即可。


class MyApplication : Application() {
lateinit var flutterEngine : FlutterEngine
override fun onCreate() {
super.onCreate()
// Instantiate a FlutterEngine.
flutterEngine = FlutterEngine(this)
// Configure an initial route.
flutterEngine.navigationChannel.setInitialRoute("your/route/here");
// Start executing Dart code to pre-warm the FlutterEngine.
flutterEngine.dartExecutor.executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
)
// Cache the FlutterEngine to be used by FlutterActivity or FlutterFragment.
FlutterEngineCache
.getInstance()
.put("my_engine_id", flutterEngine)
}
}

六、与Flutter通信


经过上面的操作,我们已经能够完成原生Android 跳转Flutter,那如何实现Flutter跳转原生Activity或者Flutter如何销毁自己返回原生页面呢?此时就用到了Flutter和原生Android的通迅机制,即Channel,分别是MethodChannel、EventChannel和BasicMessageChannel。



  • MethodChannel:用于传递方法调用,是比较常用的PlatformChannel。

  • EventChannel: 用于传递事件。

  • BasicMessageChannel:用于传递数据。


对于这种简单的跳转操作,直接使用MethodChannel即可完成。首先,我们在flutter_module中新建一个PluginManager的类,然后添加如下代码。


import 'package:flutter/services.dart';

class PluginManager {
static const MethodChannel _channel = MethodChannel('plugin_demo');

static Future<String> pushFirstActivity(Map params) async {
String resultStr = await _channel.invokeMethod('jumpToMain', params);
return resultStr;
}

}

然后,当我们点击Flutter入口页面的返回按钮时,添加一个返回的方法,主要是调用PluginManager发送消息,如下。


Future<void> backToNative() async {
String result;
try {
result = await PluginManager.pushFirstActivity({'key': 'value'});
} on PlatformException {
result = '失败';
}
print('backToNative: '+result);
}

接下来,重新使用flutter build aar重新编译aar包,并在原生Android的Flutter入口页面的configureFlutterEngine方法中添加如下代码。


class FlutterContainerActivity : FlutterActivity() {

private val CHANNEL = "plugin_demo"

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

}


override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine)
MethodChannel(flutterEngine.dartExecutor, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == "jumpToMain") {
val params = call.argument<String>("key")
Toast.makeText(this,"返回原生页面",Toast.LENGTH_SHORT).show()
finish()
result.success(params)
} else {
result.notImplemented()
}
}
}

}

重新运行原生项目时,点击Flutter左上角的返回按钮就可以返回到原生页面,其他的混合跳转也可以使用这种方式进行解决。


在这里插入图片描述


关于混合开发中混合路由和FlutterEngine多实例的问题,可以参考FlutterBoost


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

前端 PDF 水印方案

web
场景:前端下载 pdf 文件的时候,需要加上水印,再反给用户下载 用到的库:pdf-lib (文档) @pdf-lib/fontkit 字体:github 方案目标:logo图 + 中文 + 英文 + 数字 => 透明水印首先安装 pdf-lib: 它是...
继续阅读 »

场景:前端下载 pdf 文件的时候,需要加上水印,再反给用户下载
用到的库pdf-lib (文档) @pdf-lib/fontkit
字体github
方案目标:logo图 + 中文 + 英文 + 数字 => 透明水印


首先安装 pdf-lib: 它是前端创建和修改 PDF 文档的一个工具(默认不支持中文,需要加载自定义字体文件)

npm install --save pdf-lib

安装 @pdf-lib/fontkit:为 pdf-lib 加载自定义字体的工具

npm install --save @pdf-lib/fontkit

没有使用pdf.js的原因是因为:

  1. 会将 PDF 转成图片,无法选中

  2. 操作后 PDF 会变模糊

  3. 文档体积会变得异常大


实现:

首先我们的目标是在 PDF 文档中,加上一个带 logo 的,同时包含中文、英文、数字字符的透明水印,所以我们先来尝试着从本地加载一个文件,一步步搭建。

1. 获取 PDF 文件

本地:

// <input type="file" name="pdf" id="pdf-input">

let input = document.querySelector('#pdf-input');
input.onchange = onFileUpload;

// 上传文件
function onFileUpload(e) {
let event = window.event || e;

let file = event.target.files[0];
}

除了本地上传文件之外,我们也可以通过网络请求一个 PDF 回来,注意响应格式为 blob
网络:

var x = new XMLHttpRequest();
x.open("GET", url, true);
x.responseType = 'blob';
x.onload = function (e) {
let file = x.response;
}
x.send();

// 获取直接转成 pdf-lib 需要的 arrayBuffer
// const fileBytes = await fetch(url).then(res => res.arrayBuffer())

2. 文字水印

在获取到 PDF 文件数据之后,我们通过 pdf-lib 提供的接口来对文档做修改。

// 修改文档
async function modifyPdf(file) {
const pdfDoc = await PDFDocument.load(await file.arrayBuffer());

// 加载内置字体
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Courier);

// 获取文档所有页
const pages = pdfDoc.getPages();

// 文字渲染配置
const drawTextParams = {
  lineHeight: 50,
  font: helveticaFont,
  size: 12,
  color: rgb(0.08, 0.08, 0.2),
  rotate: degrees(15),
  opacity: 0.5,
};

for (let i = 0; i < pages.length; i++) {
  const page = pages[i];

  // 获取当前页宽高
  const { width, height } = page.getSize();

  // 要渲染的文字内容
  let text = "water 121314";

  for (let ix = 1; ix < width; ix += 230) { // 水印横向间隔
    let lineNum = 0;
    for (let iy = 50; iy <= height; iy += 110) { // 水印纵向间隔
      lineNum++;
       
      page.drawText(text, {
        x: lineNum & 1 ? ix : ix + 70,
        y: iy,
        ...drawTextParams,
      });
    }
  }
}

来看一下现在的效果

3. 加载本地 logo

在加载图片这块,我们最终想要的其实是图片的 Blob 数据,获取网图的话,这里就不做介绍了,下边主要着重介绍一下,如何通过 js 从本地加载一张图。
先贴上代码:

//  加载 logo blob 数据
~(function loadImg() {
let img = new Image();
img.src = "./water-logo.png";

let canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d");

img.crossOrigin = "";
img.onload = function () {
  canvas.width = this.width;
  canvas.height = this.height;

  ctx.fillStyle = "rgba(255, 255, 255, 1)";
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  ctx.drawImage(this, 0, 0, this.width, this.height);
  canvas.toBlob(
    function (blob) {
      imgBytes = blob; // 保存数据到 imgBytes 中
    },
    "image/jpeg",
    1
  ); // 参数为输出质量
};
})();

首先通过一个自执行函数,在初期就自动加载 logo 数据,当然我们也可以根据实际情况做相应的优化。
整体的思路就是,首先通过 image 元素来加载本地资源,再将 img 渲染到 canvas 中,再通过 canvas 的 toBlob 来得到我们想要的数据。

在这块我们需要注意两行代码:

ctx.fillStyle = "rgba(255, 255, 255, 1)"; 
ctx.fillRect(0, 0, canvas.width, canvas.height);

如果我们不加这两行代码的话,同时本地图片还是透明图,最后我们得到的数据将会是一个黑色的方块。所以我们需要在 drawImage 之前,用白色填充一下 canvas 。

4. 渲染 logo

在渲染 logo 图片到 PDF 文档上之前,我们还需要和加载字体类似的,把图片数据也挂载到 pdf-lib 创建的文档对象上(pdfDoc),其中 imgBytes 是我们已经加载好的图片数据。

let _img = await pdfDoc.embedJpg(await imgBytes.arrayBuffer());

挂载完之后,做一些个性化的配置

page.drawImage(_img, {
x: lineNum & 1 ? ix - 18 : ix + 70 - 18, // 奇偶行的坐标
y: iy - 8,
width: 15,
height: 15,
opacity: 0.5,
});

5. 查看文档

这一步的思路就是先通过 pdf-lib 提供的 save 方法,得到最后的文档数据,将数据转成 Blob,最后通过 a 标签打开查看。

// 保存文档 Serialize the PDFDocument to bytes (a Uint8Array)
const pdfBytes = await pdfDoc.save();

let blobData = new Blob([pdfBytes], { type: "application/pdf;Base64" });

// 新标签页预览
let a = document.createElement("a");
a.target = "_blank";
a.href = window.URL.createObjectURL(blobData);
document.body.appendChild(a);
a.click();
document.body.removeChild(a);

到目前的效果

6. 中文字体

由于默认的 pdf-lib 是不支持渲染中文的
Uncaught (in promise) Error: WinAnsi cannot encode "水" (0x6c34)

所以我们需要加载自定义字体,但是常规的字体文件都会很大,为了使用,需要将字体文件压缩一下,压缩好的字体在文档头部,包含空格和基础的3500字符。
压缩字体用到的是 gulp-fontmin 命令行工具,不是客户端。具体压缩方法,可自行搜索。

在拿到字体之后(ttf文件),将字体文件上传到网上,再拿到其 arrayBuffer 数据。之后再结合 pdf-lib 的文档对象,对字体进行注册和挂载。同时记得将文字渲染的字体配置改过来。

// 加载自定义字体
const url = 'https://xxx.xxx/xxxx';
const fontBytes = await fetch(url).then((res) => res.arrayBuffer());

// 自定义字体挂载
pdfDoc.registerFontkit(fontkit)
const customFont = await pdfDoc.embedFont(fontBytes)

// 文字渲染配置
const drawTextParams = {
  lineHeight: 50,
  font: customFont, // 改字体配置
  size: 12,
  color: rgb(0.08, 0.08, 0.2),
  rotate: degrees(15),
  opacity: 0.5,
};

所以到现在的效果

7. 完整代码

import { PDFDocument, StandardFonts, rgb, degrees } from "pdf-lib";
import fontkit from "@pdf-lib/fontkit";

let input = document.querySelector("#pdf-input");
let imgBytes;

input.onchange = onFileUpload;

// 上传文件
function onFileUpload(e) {
let event = window.event || e;

let file = event.target.files[0];
console.log(file);
if (file.size) {
  modifyPdf(file);
}
}

// 修改文档
async function modifyPdf(file) {
const pdfDoc = await PDFDocument.load(await file.arrayBuffer());

// 加载内置字体
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Courier);

// 加载自定义字体
const url = 'pttps://xxx.xxx/xxx';
const fontBytes = await fetch(url).then((res) => res.arrayBuffer());

// 自定义字体挂载
pdfDoc.registerFontkit(fontkit)
const customFont = await pdfDoc.embedFont(fontBytes)

// 获取文档所有页
const pages = pdfDoc.getPages();

// 文字渲染配置
const drawTextParams = {
  lineHeight: 50,
  font: customFont,
  size: 12,
  color: rgb(0.08, 0.08, 0.2),
  rotate: degrees(15),
  opacity: 0.5,
};

let _img = await pdfDoc.embedJpg(await imgBytes.arrayBuffer());

for (let i = 0; i < pages.length; i++) {
  const page = pages[i];

  // 获取当前页宽高
  const { width, height } = page.getSize();

  // 要渲染的文字内容
  let text = "水印 water 121314";

  for (let ix = 1; ix < width; ix += 230) { // 水印横向间隔
    let lineNum = 0;
    for (let iy = 50; iy <= height; iy += 110) { // 水印纵向间隔
      lineNum++;
      page.drawImage(_img, {
        x: lineNum & 1 ? ix - 18 : ix + 70 - 18,
        y: iy - 8,
        width: 15,
        height: 15,
        opacity: 0.7,
      });
      page.drawText(text, {
        x: lineNum & 1 ? ix : ix + 70,
        y: iy,
        ...drawTextParams,
      });
    }
  }
}

// 保存文档 Serialize the PDFDocument to bytes (a Uint8Array)
const pdfBytes = await pdfDoc.save();

let blobData = new Blob([pdfBytes], { type: "application/pdf;Base64" });

// 新标签页预览
let a = document.createElement("a");
a.target = "_blank";
a.href = window.URL.createObjectURL(blobData);
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}

// 加载 logo blob 数据
~(function loadImg() {
let img = new Image();
img.src = "./water-logo.png";

let canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d");

img.crossOrigin = "";
img.onload = function () {
  canvas.width = this.width;
  canvas.height = this.height;

  ctx.fillStyle = "rgba(255, 255, 255, 1)";
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  ctx.drawImage(this, 0, 0, this.width, this.height);
  canvas.toBlob(
    function (blob) {
      imgBytes = blob;
    },
    "image/jpeg",
    1
  ); // 参数为输出质量
};
})();

8. 不完美的地方

当前方案虽然可以实现在前端为 PDF 加水印,但是由于时间关系,有些瑕疵还需要再进一步探索解决 💪:

  1. 水印是浮在原文本之上的,可以被选中

  2. logo 的背景虽然不注意看不到,但是实际上还未完全透明 🤔

来源:http://www.cnblogs.com/iamzhiyudong/p/14990528.html

收起阅读 »

Logstash:如何在 Elasticsearch 中查找和删除重复文档

许多将数据驱动到 Elasticsearch 中的系统将利用 Elasticsearch 为新插入的文档自动生成的 id 值。 但是,如果数据源意外地将同一文档多次发送到Elasticsearch,并且如果将这种自动生成的 id 值用于 Elasticsear...
继续阅读 »

许多将数据驱动到 Elasticsearch 中的系统将利用 Elasticsearch 为新插入的文档自动生成的 id 值。 但是,如果数据源意外地将同一文档多次发送到Elasticsearch,并且如果将这种自动生成的 id 值用于 Elasticsearch 插入的每个文档,则该同一文档将使用不同的id值多次存储在 Elasticsearch 中。 如果发生这种情况,那么可能有必要找到并删除此类重复项。 因此,在此博客文章中,我们介绍如何通过

  • 使用 Logstash

  • 使用 Python 编写的自定义代码从 Elasticsearch 中检测和删除重复文档

示例文档结构

就本博客而言,我们假设 Elasticsearch 集群中的文档具有以下结构。 这对应于包含代表股票市场交易的文档的数据集。

{
   "_index": "stocks",
   "_type": "doc",
   "_id": "6fo3tmMB_ieLOlkwYclP",
   "_version": 1,
   "found": true,
   "_source": {
       "CAC": 1854.6,
       "host": "Alexanders-MBP",
       "SMI": 2061.7,
       "@timestamp": "2017-01-09T02:30:00.000Z",
       "FTSE": 2827.5,
       "DAX": 1527.06,
       "time": "1483929000",
       "message": "1483929000,1527.06,2061.7,1854.6,2827.5\r",
       "@version": "1"
   }
}

给定该示例文档结构,出于本博客的目的,我们任意假设如果多个文档的 [“CAC”,“FTSE”,“SMI”] 字段具有相同的值,则它们是彼此重复的。

使用 Logstash 对 Elasticsearch 文档进行重复数据删除

这种方法已经在之前的文章 “Logstash:处理重复的文档” 已经描述过了。Logstash 可用于检测和删除 Elasticsearch 索引中的重复文档。 在那个文章中,我们已经对这个方法进行了详述,也做了展示。我们也无妨做一个更进一步的描述。

在下面的示例中,我编写了一个简单的 Logstash 配置,该配置从 Elasticsearch 集群上的索引读取文档,然后使用指纹过滤器根据 ["CAC", "FTSE", "SMI"] 字段的哈希值为每个文档计算唯一的 _id 值,最后将每个文档写回到同一 Elasticsearch 集群上的新索引,这样重复的文档将被写入相同的 _id 并因此被消除。

此外,通过少量修改,相同的 Logstash 过滤器也可以应用于写入新创建的索引的将来文档,以确保几乎实时删除重复项。这可以通过更改以下示例中的输入部分以接受来自实时输入源的文档,而不是从现有索引中提取文档来实现。

请注意,使用自定义 id 值(即不是由 Elasticsearch 生成的 _id)将对索引操作的[写入性能产生一些影响](https://www.elastic.co/guide/en/elasticsearch/reference/master/tune-for-indexing-speed.html#use_auto_generated_ids)。

另外,值得注意的是,根据所使用的哈希算法,此方法理论上可能会导致 id 值的[哈希冲突数](https://en.wikipedia.org/wiki/Collision(computer_science))不为零,这在理论上可能导致两个不相同的文档映射到相同的_id,因此导致这些文档之一丢失。对于大多数实际情况,哈希冲突的可能性可能非常低。对不同哈希函数的详细分析不在本博客的讨论范围之内,但是应仔细考虑指纹过滤器中使用的哈希函数,因为它将影响提取性能和哈希冲突次数。

下面给出了使用指纹过滤器对现有索引进行重复数据删除的简单 Logstash 配置。

input {
# Read all documents from Elasticsearch
elasticsearch {
  hosts => "localhost"
  index => "stocks"
  query => '{ "sort": [ "_doc" ] }'
}
}
# This filter has been updated on February 18, 2019
filter {
  fingerprint {
      key => "1234ABCD"
      method => "SHA256"
      source => ["CAC", "FTSE", "SMI"]
      target => "[@metadata][generated_id]"
      concatenate_sources => true # <-- New line added since original post date
  }
}
output {
  stdout { codec => dots }
  elasticsearch {
      index => "stocks_after_fingerprint"
      document_id => "%{[@metadata][generated_id]}"
  }
}

用于 Elasticsearch 文档重复数据删除的自定义 Python 脚本

内存有效的方法

如果不使用 Logstash,则可以使用自定义 python 脚本有效地完成重复数据删除。 对于这种方法,我们计算定义为唯一标识文档的["CAC","FTSE","SMI"] 字段的哈希值 (Hash)。 然后,我们将此哈希用作 python 字典中的键,其中每个字典条目的关联值将是映射到同一哈希的文档 _id 的数组。

如果多个文档具有相同的哈希,则可以删除映射到相同哈希的重复文档。 另外,如果你担心哈希值冲突的可能性,则可以检查映射到同一散列的文档的内容,以查看文档是否确实相同,如果是,则可以消除重复项。

检测算法分析

对于 50GB 的索引,如果我们假设索引包含平均大小为 0.4 kB 的文档,则索引中将有1.25亿个文档。 在这种情况下,使用128位 md5 哈希将重复数据删除数据结构存储在内存中所需的内存量约为128位x 125百万= 2GB 内存,再加上160位_id将需要另外160位x 125百万= 2.5 GB 的内存。 因此,此算法将需要4.5GB 的 RAM 数量级,以将所有相关的数据结构保留在内存中。 如果可以应用下一节中讨论的方法,则可以大大减少内存占用。

算法增强

在本节中,我们对算法进行了增强,以减少内存使用以及连续删除新的重复文档。

如果你要存储时间序列数据,并且知道重复的文档只会在彼此之间的一小段时间内出现,那么您可以通过在文档的子集上重复执行该算法来改善该算法的内存占用量在索引中,每个子集对应一个不同的时间窗口。例如,如果您有多年的数据,则可以在datetime字段(在过滤器上下文中以获得最佳性能)上使用范围查询,一次仅一周查看一次数据集。这将要求算法执行52次(每周一次)-在这种情况下,这种方法将使最坏情况下的内存占用减少52倍。

在上面的示例中,你可能会担心没有检测到跨星期的重复文档。假设你知道重复的文档间隔不能超过2小时。然后,您需要确保算法的每次执行都包含与之前算法执行过的最后一组文档重叠2小时的文档。对于每周示例,因此,您需要查询170小时(1周+ 2小时)的时间序列文档,以确保不会丢失任何重复项。

如果你希望持续定期从索引中清除重复的文档,则可以对最近收到的文档执行此算法。与上述逻辑相同-确保分析中包括最近收到的文档以及与稍旧的文档的足够重叠,以确保不会无意中遗漏重复项。

用于检测重复文档的 Python 代码

以下代码演示了如何可以有效地评估文档以查看它们是否相同,然后根据需要将其删除。 但是,为了防止意外删除文档,在本示例中,我们实际上并未执行删除操作。 这样的功能的实现将是非常直接的。

可以在 github 上找到用于从 Elasticsearch 中删除文档重复数据的代码。

#!/usr/local/bin/python3
import hashlib
from elasticsearch import Elasticsearch
es = Elasticsearch(["localhost:9200"])
dict_of_duplicate_docs = {}
# The following line defines the fields that will be
# used to determine if a document is a duplicate
keys_to_include_in_hash = ["CAC", "FTSE", "SMI"]
# Process documents returned by the current search/scroll
def populate_dict_of_duplicate_docs(hits):
   for item in hits:
       combined_key = ""
       for mykey in keys_to_include_in_hash:
           combined_key += str(item['_source'][mykey])
       _id = item["_id"]
       hashval = hashlib.md5(combined_key.encode('utf-8')).digest()
       # If the hashval is new, then we will create a new key
       # in the dict_of_duplicate_docs, which will be
       # assigned a value of an empty array.
       # We then immediately push the _id onto the array.
       # If hashval already exists, then
       # we will just push the new _id onto the existing array
       dict_of_duplicate_docs.setdefault(hashval, []).append(_id)
# Loop over all documents in the index, and populate the
# dict_of_duplicate_docs data structure.
def scroll_over_all_docs():
   data = es.search(index="stocks", scroll='1m',  body={"query": {"match_all": {}}})
   # Get the scroll ID
   sid = data['_scroll_id']
   scroll_size = len(data['hits']['hits'])
   # Before scroll, process current batch of hits
   populate_dict_of_duplicate_docs(data['hits']['hits'])
   while scroll_size > 0:
       data = es.scroll(scroll_id=sid, scroll='2m')
       # Process current batch of hits
       populate_dict_of_duplicate_docs(data['hits']['hits'])
       # Update the scroll ID
       sid = data['_scroll_id']
       # Get the number of results that returned in the last scroll
       scroll_size = len(data['hits']['hits'])
def loop_over_hashes_and_remove_duplicates():
   # Search through the hash of doc values to see if any
   # duplicate hashes have been found
   for hashval, array_of_ids in dict_of_duplicate_docs.items():
     if len(array_of_ids) > 1:
       print("********** Duplicate docs hash=%s **********" % hashval)
       # Get the documents that have mapped to the current hashval
       matching_docs = es.mget(index="stocks", doc_type="doc", body={"ids": array_of_ids})
       for doc in matching_docs['docs']:
           # In this example, we just print the duplicate docs.
           # This code could be easily modified to delete duplicates
           # here instead of printing them
           print("doc=%s\n" % doc)
def main():
   scroll_over_all_docs()
   loop_over_hashes_and_remove_duplicates()
main()

结论

在此博客文章中,我们展示了两种在 Elasticsearch 中对文档进行重复数据删除的方法。 第一种方法使用 Logstash 删除重复的文档,第二种方法使用自定义的 Python 脚本查找和删除重复的文档。

来源:https://blog.csdn.net/UbuntuTouch/article/details/106643400

原文: How to Find and Remove Duplicate Documents in Elasticsearch | Elastic Blog

收起阅读 »

盘点程序员写过的惊天Bug:亏损30亿、致6人死亡,甚至差点毁灭世界

一个Bug就地蒸发5亿美元;软件设计层面出Bug致6人死亡;DeBug不成功直接世界毁灭。你职业生涯中写过最大的Bug是什么?在这个问题上,勇敢的码农们,总是能不断地创造奇迹。这不禁让路过的一位普通市民感叹:感觉有你们,我们还活在这个世界就像死神来了Bug很大...
继续阅读 »

一个Bug就地蒸发5亿美元;

软件设计层面出Bug致6人死亡;

DeBug不成功直接世界毁灭。

你职业生涯中写过最大的Bug是什么?

在这个问题上,勇敢的码农们,总是能不断地创造奇迹。

这不禁让路过的一位普通市民感叹:

感觉有你们,我们还活在这个世界就像死神来了

Bug很大,你忍一下

一个Bug到底能有多大?

几个历史数据转储逻辑Bug或发货逻辑Bug,就能让几十万轻松蒸发:


你们这亏钱的Bug都洒洒水啦,写Bug差点进去的见过没?

马上就有码农站出来表示不服,并表示自己参与开发的一款发薪软件曾出现Bug,会导致发放的薪资变成双倍,总共能多发2000多万

当时查出Bug的时候发薪单已经生成,就差批量任务向银行发起请求了!


奇怪的胜负心就这么燃起来了。

一时间,什么水闸关不住、高铁追尾、甚至差点导致非洲国家内战的Bug都来了。


如果再放眼全球,你就会发现——Bug没有最大,只有更大。

2016年时,Excel就出过一个致使上万份遗传基因学论文出错的Bug。

很多长得像日期表达的长基因名的缩写(比如SEPT2、MARCH1),会在这一Bug的作用下被Excel自动转化成日期格式:


学术领域之外的Bug那就更牛逼了。

比如在1996年,欧洲运载火箭Ariane 5在发射37秒后当场爆炸。

一瞬间,70亿美元的开发费用全部木大,5亿美元的设备原地蒸发。

这一切都由一个整数溢出(Integer Overflow)的Bug引起。


而如果翻开维基百科上的这份专门统计历史上造成严重后果的Bug清单,沿着12个类别一个一个找下去,就会发现——

几乎每一条Bug的背后都存在着千万上亿的金钱损失。


有时,甚至会带来意外死亡。

1985年到1987年间,由加拿大AECL公司开发的Therac-25放射线疗法机器在软件互锁机制上出现了Bug,从而使辐射能量变成了正常剂量的100倍

最终,至少有6名来自美国和加拿大地区的患者由于遭受过量辐射而意外死亡。


还有差点引发全球核战争的Bug:1983年苏联核警报误报事件


苏联军官Stanislav Yevgrafovich Petrov

在那一年的9月26日,苏联的雷达监测到了5枚自美军基地发射而来的导弹。

而上图的这位苏联军官权衡再三,最终将这一导弹攻击警告判断为误报,并没有按照规定向上级汇报并申请反击。

事实证明,这次DeBug成功避免了地球Online在1983年就发生重启。

“不是Bug是特性”

看完了上面那些惊天大活儿,瞬间觉得邮件/短信连环CALL这种Bug都温柔了许多。

像这种由于抽奖程序Bug导致的社死,好像也不是个事儿了:


而影响力又大,又没有造成严重损失,甚至让用户拍手叫好的Bug也不是没有。

比如一到游戏圈,Bug就会自动改名为特性


原神鱼竿Bug

某些知名游戏大厂甚至还会联名发布Bug马克杯,玩梗玩得飞起。


还有玩家真情实感地表示:Bug正是游戏复杂规则和交互的体现,我游YYDS!


《矮人要塞》猫咪离奇死亡事件

甚至在游戏行业之外,还有用户在Bug被修复后愤怒投诉:


图源知乎答主三和四保

最后,再回到“你的程序员生涯中写过的最大Bug”这一问题上来。

有回答选择直接结束比赛:

你们的程序员生涯中写过的最大Bug是什么?——当初选择了做程序员。


软件Bug清单:
https://en.wikipedia.org/wiki/List_of_software_Bugs

参考链接:
https://www.zhihu.com/question/482967292

来源:量子位

收起阅读 »

【集成教程】环信Android UI库导入并实现一些基础功能

EaseIMKit 是什么?

EaseIMKit 是基于环信 IM SDK 的一款 UI 组件库,它提供了一些通用的 UI 组件,例如‘会话列表’、‘聊天界面’和‘联系人列表’等,开发者可根据实际业务需求通过该组件库快速地搭建自定义 IM 应用。EaseIMKit 中的组件在实现 UI 功能的同时,调用 IM SDK 相应的接口实现 IM 相关逻辑和数据的处理,因而开发者在使用EaseIMKit 时只需关注自身业务或个性化扩展即可。

下面详细教大家如何导入环信UI库并实现以下基础功能。

一、 如何修改会话列表(ConversationListFragment)的整体样式
二、如何修改会话条目大标题颜色和小标题颜色
三、如何去掉发送语音时未读的红色圆点
四、如何修改emoji图片
五、如何修改名片消息ui布局
六、发送视频更改ui布局
七、如何修改会话条目分割线宽高
八、如何修改气泡颜色

导入

如果是刚开始集成的小伙伴,建议sdk的版本号和ui库的版本号保持一致

1.首先我们打开环信Android端文档,点击Easeimkit使用指南



2.在简介下方有EaseIMKit源码地址



3.Github地址上点击tags,我们来找自己对应的版本号




4.点击我们sdk对应的版本号进行下载



5.以moudel的形式将easeimkit ui库导入



6.修改build.gradle中的远程库,红色圈中正常我们不引入ui库的话就是需要将注释的依赖正常打开,如果我们导入ui库格式应
api (project(path: ':ease-im-kit'))
黄色方圈中为Easemob的SDK的依赖




7.将settings.gradle 的
include ':ease-im-kit'
设置上去




8.导入成功我们就可以看到这个就大功告成了



实现基础功能

一、 如何修改会话列表(ConversationListFragment)的整体样式
按照个人需求自定义添加背景即可
(ease_conversation_list)




二、如何修改会话条目大标题颜色和小标题颜色(EaseConversationListLayout)
此标题控制的是会话条目上面的大标题和小的文本内容,上方包含用户的昵称下方包含用户聊天内容,具体参考注释处自定义更改



三、如何去掉发送语音时未读的红色圆点里面也同时包含了发送语音的背景颜色以及样式可以个性化的进行修改
接收方为(ease_row_received_voice) 发送方(ease_rwo_sent_voice)



四、如何修改emoji图片

EaseDefaultEmojiconDatas



五、如何修改名片消息ui布局
demo_activity_send_user_card为发送方



六、发送视频更改ui布局
发送方:ease_row_sent_video 接收方:ease_row_received_video



七、如何修改会话条目分割线宽高
ease_item_row_chat_history



八、如何修改气泡颜色
接收方ease_row_received_message ,发送方ease_row_sent_message



美团动态线程池实践思路,开源了

写在前面 稍微有些Java编程经验的小伙伴都知道,Java的精髓在juc包,这是大名鼎鼎的Doug Lea老爷 子的杰作,评价一个程序员Java水平怎么样,一定程度上看他对juc包下的一些技术掌握的怎么样,这也是面试中的基本上必问的一些技术点之一。 juc包主...
继续阅读 »

写在前面


稍微有些Java编程经验的小伙伴都知道,Java的精髓在juc包,这是大名鼎鼎的Doug Lea老爷
子的杰作,评价一个程序员Java水平怎么样,一定程度上看他对juc包下的一些技术掌握的怎么样,这也是面试中的基本上必问的一些技术点之一。


juc包主要包括:



1.原子类(AtomicXXX)


2.锁类(XXXLock)


3.线程同步类(AQS、CountDownLatch、CyclicBarrier、Semaphore、Exchanger)


4.任务执行器类(Executor体系类,包括今天的主角ThreadPoolExecutor)


5.并发集合类(ConcurrentXXX、CopyOnWriteXXX)相关集合类


6.阻塞队列类(BlockingQueue继承体系类)


7.Future相关类


8.其他一些辅助工具类



多线程编程场景下,这些类都是必备技能,会这些可以帮助我们写出高质量、高性能、少bug的代码,同时这些也是Java中比较难啃的一些技术,需要持之以恒,学以致用,在使用中感受他们带来的奥妙。


上边简单罗列了下juc包下功能分类,这篇文章我们主要来介绍动态可监控线程池的,所以具体内容也就不展开讲了,以后有时间单独来聊吧。看这篇文章前,希望读者最好有一定的线程池ThreadPoolExecutor使用经验,不然看起来会有点懵。


如果你对ThreadPoolExecutor不是很熟悉,推荐阅读下面两篇文章


javadoop: http://www.javadoop.com/post/java-t…


美团技术博客: tech.meituan.com/2020/04/02/…




背景


使用ThreadPoolExecutor过程中你是否有以下痛点呢?



1.代码中创建了一个ThreadPoolExecutor,但是不知道那几个核心参数设置多少比较合适


2.凭经验设置参数值,上线后发现需要调整,改代码重启服务,非常麻烦


3.线程池相对开发人员来说是个黑盒,运行情况不能感知到,直到出现问题



如果你有以上痛点,这篇文章要介绍的动态可监控线程池(DynamicTp)或许能帮助到你。


如果看过ThreadPoolExecutor的源码,大概可以知道其实它有提供一些set方法,可以在运行时动态去修改相应的值,这些方法有:


public void setCorePoolSize(int corePoolSize);
public void setMaximumPoolSize(int maximumPoolSize);
public void setKeepAliveTime(long time, TimeUnit unit);
public void setThreadFactory(ThreadFactory threadFactory);
public void setRejectedExecutionHandler(RejectedExecutionHandler handler);

现在大多数的互联网项目其实都会微服务化部署,有一套自己的服务治理体系,微服务组件中的分布式配置中心扮演的就是动态修改配置,实时生效的角色。那么我们是否可以结合配置中心来做运行时线程池参数的动态调整呢?答案是肯定的,而且配置中心相对都是高可用的,使用它也不用过于担心配置推送出现问题这类事儿,而且也能减少研发动态线程池组件的难度和工作量。


综上,我们总结出以下的背景



  • 广泛性:在Java开发中,想要提高系统性能,线程池已经是一个90%以上的人都会选择使用的基础工具

  • 不确定性:项目中可能会创建很多线程池,既有IO密集型的,也有CPU密集型的,但线程池的参数并不好确定;需要有套机制在运行过程中动态去调整参数

  • 无感知性,线程池运行过程中的各项指标一般感知不到;需要有套监控报警机制在事前、事中就能让开发人员感知到线程池的运行状况,及时处理

  • 高可用性,配置变更需要及时推送到客户端;需要有高可用的配置管理推送服务,配置中心是现在大多数互联网系统都会使用的组件,与之结合可以大幅度减少开发量及接入难度




简介


我们基于配置中心对线程池ThreadPoolExecutor做一些扩展,实现对运行中线程池参数的动态修改,实时生效;以及实时监控线程池的运行状态,触发设置的报警策略时报警,报警信息会推送办公平台(钉钉、企微等)。报警维度包括(队列容量、线程池活性、拒绝触发等);同时也会定时采集线程池指标数据供监控平台可视化使用。使我们能时刻感知到线程池的负载,根据情况及时调整,避免出现问题影响线上业务。


    |  __ \                            (_) |__   __|
| | | |_ _ _ __ __ _ _ __ ___ _ ___| |_ __
| | | | | | | '_ \ / _` | '_ ` _ | |/ __| | '_ \
| |__| | |_| | | | | (_| | | | | | | | (__| | |_) |
|_____/ __, |_| |_|__,_|_| |_| |_|_|___|_| .__/
__/ | | |
|___/ |_|
:: Dynamic Thread Pool ::

特性



  • 参考美团线程池实践 ,对线程池参数动态化管理,增加监控、报警功能

  • 基于Spring框架,现只支持SpringBoot项目使用,轻量级,引入starter即可食用

  • 基于配置中心实现线程池参数动态调整,实时生效;集成主流配置中心,默认支持Nacos、Apollo,同时也提供SPI接口可自定义扩展实现

  • 内置通知报警功能,提供多种报警维度(配置变更通知、活性报警、容量阈值报警、拒绝策略触发报警),默认支持企业微信、钉钉报警,同时提供SPI接口可自定义扩展实现

  • 内置线程池指标采集功能,支持通过MicroMeter、JsonLog日志输出、Endpoint三种方式,可通过SPI接口自定义扩展实现

  • 集成管理常用第三方组件的线程池,已集成SpringBoot内置WebServer(Tomcat、Undertow、Jetty)的线程池管理




架构设计


主要分四大模块




  • 配置变更监听模块:


    1.监听特定配置中心的指定配置文件(默认实现Nacos、Apollo),可通过内部提供的SPI接口扩展其他实现


    2.解析配置文件内容,内置实现yml、properties配置文件的解析,可通过内部提供的SPI接口扩展其他实现


    3.通知线程池管理模块实现刷新




  • 线程池管理模块:


    1.服务启动时从配置中心拉取配置信息,生成线程池实例注册到内部线程池注册中心中


    2.监听模块监听到配置变更时,将变更信息传递给管理模块,实现线程池参数的刷新


    3.代码中通过getExecutor()方法根据线程池名称来获取线程池对象实例




  • 监控模块:


    实现监控指标采集以及输出,默认提供以下三种方式,也可通过内部提供的SPI接口扩展其他实现


    1.默认实现Json log输出到磁盘


    2.MicroMeter采集,引入MicroMeter相关依赖


    3.暴雷Endpoint端点,可通过http方式访问




  • 通知告警模块:


    对接办公平台,实现通告告警功能,默认实现钉钉、企微,可通过内部提供的SPI接口扩展其他实现,通知告警类型如下


    1.线程池参数变更通知


    2.阻塞队列容量达到设置阈值告警


    3.线程池活性达到设置阈值告警


    4.触发拒绝策略告警







使用



  • maven依赖



  1. apollo应用用接入用此依赖
        <dependency>
    <groupId>io.github.lyh200</groupId>
    <artifactId>dynamic-tp-spring-boot-starter-apollo</artifactId>
    <version>1.0.0</version>
    </dependency>


  2. spring-cloud场景下的nacos应用接入用此依赖
        <dependency>
    <groupId>io.github.lyh200</groupId>
    <artifactId>dynamic-tp-spring-cloud-starter-nacos</artifactId>
    <version>1.0.0</version>
    </dependency>


  3. 非spring-cloud场景下的nacos应用接入用此依赖
        <dependency>
    <groupId>io.github.lyh200</groupId>
    <artifactId>dynamic-tp-spring-boot-starter-nacos</artifactId>
    <version>1.0.0</version>
    </dependency>





  • 线程池配置


    spring:
    dynamic:
    tp:
    enabled: true
    enabledBanner: true # 是否开启banner打印,默认true
    enabledCollect: false # 是否开启监控指标采集,默认false
    collectorType: logging # 监控数据采集器类型(JsonLog | MicroMeter),默认logging
    logPath: /home/logs # 监控日志数据路径,默认${user.home}/logs
    monitorInterval: 5 # 监控时间间隔(报警判断、指标采集),默认5s
    nacos: # nacos配置,不配置有默认值(规则name-dev.yml这样)
    dataId: dynamic-tp-demo-dev.yml
    group: DEFAULT_GROUP
    apollo: # apollo配置,不配置默认拿apollo配置第一个namespace
    namespace: dynamic-tp-demo-dev.yml
    configType: yml # 配置文件类型
    platforms: # 通知报警平台配置
    - platform: wechat
    urlKey: 3a7500-1287-4bd-a798-c5c3d8b69c # 替换
    receivers: test1,test2 # 接受人企微名称
    - platform: ding
    urlKey: f80dad441fcd655438f4a08dcd6a # 替换
    secret: SECb5441fa6f375d5b9d21 # 替换,非sign模式可以没有此值
    receivers: 15810119805 # 钉钉账号手机号
    tomcatTp: # tomcat web server线程池配置
    minSpare: 100
    max: 400
    jettyTp: # jetty web server线程池配置
    min: 100
    max: 400
    undertowTp: # undertow web server线程池配置
    ioThreads: 100
    workerThreads: 400
    executors: # 动态线程池配置
    - threadPoolName: dynamic-tp-test-1
    corePoolSize: 6
    maximumPoolSize: 8
    queueCapacity: 200
    queueType: VariableLinkedBlockingQueue # 任务队列,查看源码QueueTypeEnum枚举类
    rejectedHandlerType: CallerRunsPolicy # 拒绝策略,查看RejectedTypeEnum枚举类
    keepAliveTime: 50
    allowCoreThreadTimeOut: false
    threadNamePrefix: test # 线程名前缀
    notifyItems: # 报警项,不配置自动会配置(变更通知、容量报警、活性报警、拒绝报警)
    - type: capacity # 报警项类型,查看源码 NotifyTypeEnum枚举类
    enabled: true
    threshold: 80 # 报警阈值
    platforms: [ding,wechat] # 可选配置,不配置默认拿上层platforms配置的所以平台
    interval: 120 # 报警间隔(单位:s)
    - type: change
    enabled: true
    - type: liveness
    enabled: true
    threshold: 80
    - type: reject
    enabled: true
    threshold: 1



  • 代码方式生成,服务启动会自动注册


    @Configuration
    public class DtpConfig {

    @Bean
    public DtpExecutor demo1Executor() {
    return DtpCreator.createDynamicFast("demo1-executor");
    }

    @Bean
    public ThreadPoolExecutor demo2Executor() {
    return ThreadPoolBuilder.newBuilder()
    .threadPoolName("demo2-executor")
    .corePoolSize(8)
    .maximumPoolSize(16)
    .keepAliveTime(50)
    .allowCoreThreadTimeOut(true)
    .workQueue(QueueTypeEnum.SYNCHRONOUS_QUEUE.getName(), null, false)
    .rejectedExecutionHandler(RejectedTypeEnum.CALLER_RUNS_POLICY.getName())
    .buildDynamic();
    }
    }



  • 代码调用,根据线程池名称获取


    public static void main(String[] args) {
    DtpExecutor dtpExecutor = DtpRegistry.getExecutor("dynamic-tp-test-1");
    dtpExecutor.execute(() -> System.out.println("test"));
    }





注意事项




  1. 配置文件配置的参数会覆盖通过代码生成方式配置的参数




  2. 阻塞队列只有VariableLinkedBlockingQueue类型可以修改capacity,该类型功能和LinkedBlockingQueue相似,只是capacity不是final类型,可以修改,




VariableLinkedBlockingQueue参考RabbitMq的实现




  1. 启动看到如下日志输出证明接入成功



    | __ \ (_) |__ __|
    | | | |_ _ _ __ __ _ _ __ ___ _ ___| |_ __
    | | | | | | | '_ \ / _` | '_ ` _ | |/ __| | '_ \
    | |__| | |_| | | | | (_| | | | | | | | (__| | |_) |
    |_____/ __, |_| |_|__,_|_| |_| |_|_|___|_| .__/
    __/ | | |
    |___/ |_|
    :: Dynamic Thread Pool ::

    DynamicTp register, executor: DtpMainPropWrapper(dtpName=dynamic-tp-test-1, corePoolSize=6, maxPoolSize=8, keepAliveTime=50, queueType=VariableLinkedBlockingQueue, queueCapacity=200, rejectType=RejectedCountableCallerRunsPolicy, allowCoreThreadTimeOut=false)



  2. 配置变更会推送通知消息,且会高亮变更的字段



    DynamicTp [dynamic-tp-test-2] refresh end, changed keys: [corePoolSize, queueCapacity], corePoolSize: [6 => 4], maxPoolSize: [8 => 8], queueType: [VariableLinkedBlockingQueue => VariableLinkedBlockingQueue], queueCapacity: [200 => 2000], keepAliveTime: [50s => 50s], rejectedType: [CallerRunsPolicy => CallerRunsPolicy], allowsCoreThreadTimeOut: [false => false]





通知报警


触发报警阈值会推送相应报警消息,且会高亮显示相关字段,活性告警、容量告警、拒绝告警



配置变更会推送通知消息,且会高亮变更的字段





监控日志


通过主配置文件collectType属性配置指标采集类型,默认值:logging



  • micrometer方式:通过引入micrometer相关依赖采集到相应的平台


(如Prometheus,InfluxDb...)




  • logging:指标数据以json格式输出日志到磁盘,地址logPath/dynamictp/{logPath}/ dynamictp/{appName}.monitor.log


    2022-01-16 15:25:20.599 INFO [dtp-monitor-thread-1:d.m.log] {"activeCount":2,"queueSize":100,"largestPoolSize":4,"poolSize":2,"rejectHandlerName":"CallerRunsPolicy","queueCapacity":1024,"fair":false,"rejectCount":0,"waitTaskCount":10,"taskCount":120,"queueRemainingCapacity":1024,"corePoolSize":6,"queueType":"VariableLinkedBlockingQueue","completedTaskCount":1078,"dtpName":"remoting-call","maximumPoolSize":8}
    2022-01-16 15:25:25.603 INFO [dtp-monitor-thread-1:d.m.log] {"activeCount":2,"queueSize":120,"largestPoolSize":4,"poolSize":2,"rejectHandlerName":"CallerRunsPolicy","queueCapacity":1024,"fair":false,"rejectCount":0,"waitTaskCount":20,"taskCount":140,"queueRemainingCapacity":1024,"corePoolSize":6,"queueType":"VariableLinkedBlockingQueue","completedTaskCount":1459,"dtpName":"remoting-call","maximumPoolSize":8}
    2022-01-16 15:25:30.609 INFO [dtp-monitor-thread-1:d.m.log] {"activeCount":2,"queueSize":140,"largestPoolSize":4,"poolSize":2,"rejectHandlerName":"CallerRunsPolicy","queueCapacity":1024,"fair":false,"rejectCount":0,"waitTaskCount":89,"taskCount":180,"queueRemainingCapacity":1024,"corePoolSize":6,"queueType":"VariableLinkedBlockingQueue","completedTaskCount":1890,"dtpName":"remoting-call","maximumPoolSize":8}
    2022-01-16 15:25:35.613 INFO [dtp-monitor-thread-1:d.m.log] {"activeCount":2,"queueSize":160,"largestPoolSize":4,"poolSize":2,"rejectHandlerName":"CallerRunsPolicy","queueCapacity":1024,"fair":false,"rejectCount":0,"waitTaskCount":99,"taskCount":230,"queueRemainingCapacity":1024,"corePoolSize":6,"queueType":"VariableLinkedBlockingQueue","completedTaskCount":2780,"dtpName":"remoting-call","maximumPoolSize":8}
    2022-01-16 15:25:40.616 INFO [dtp-monitor-thread-1:d.m.log] {"activeCount":2,"queueSize":230,"largestPoolSize":4,"poolSize":2,"rejectHandlerName":"CallerRunsPolicy","queueCapacity":1024,"fair":false,"rejectCount":0,"waitTaskCount":0,"taskCount":300,"queueRemainingCapacity":1024,"corePoolSize":6,"queueType":"VariableLinkedBlockingQueue","completedTaskCount":4030,"dtpName":"remoting-call","maximumPoolSize":8}



  • 暴露EndPoint端点(dynamic-tp),可以通过http方式请求


    [
    {
    "dtp_name": "remoting-call",
    "core_pool_size": 8,
    "maximum_pool_size": 16,
    "queue_type": "SynchronousQueue",
    "queue_capacity": 0,
    "queue_size": 0,
    "fair": false,
    "queue_remaining_capacity": 0,
    "active_count": 2,
    "task_count": 2760,
    "completed_task_count": 2760,
    "largest_pool_size": 16,
    "pool_size": 8,
    "wait_task_count": 0,
    "reject_count": 12462,
    "reject_handler_name": "CallerRunsPolicy"
    }
    ]




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

ARouter原理解析分享

前言 炎炎夏日,不知道大家的周末会是如何安排。本文将给大家带来的分享是ARouter的的原理介绍,通过了解它的原理,我们可以知道它是如何支持组件化和不互相依赖的模块间进行调用或者页面的跳转。 正文 1.ARouter介绍 ARouter是阿里开源的一个用于进行...
继续阅读 »

前言


炎炎夏日,不知道大家的周末会是如何安排。本文将给大家带来的分享是ARouter的的原理介绍,通过了解它的原理,我们可以知道它是如何支持组件化和不互相依赖的模块间进行调用或者页面的跳转。


正文


1.ARouter介绍


ARouter是阿里开源的一个用于进行组件化的路由框架,它可以帮助互不依赖的组件间进行页面跳转和服务调用。


2.ARouter使用


添加依赖:


android {
//...
defaultConfig {
kapt {
arguments {
arg("AROUTER_MODULE_NAME", project.getName())
}
}
}
//...
}

dependencies {
api 'com.alibaba:arouter-api:1.5.0'
kapt 'com.alibaba:arouter-compiler:1.2.2'
}

定义跳转Activity的path:


@Route(path = "/test/router_activity")
class RouterActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_router)
}
}

初始化Router框架:


class RouterDemoApp : Application() {
override fun onCreate() {
super.onCreate()
//初始化、注入
ARouter.init(this)
}
}

调用跳转:


ARouter.getInstance().build("/test/router_activity").navigation()

3.生成的代码(生成的路由表)


当我们给Activity或者服务等加上Route注解后,build一下,ARouter框架便会按照模版帮我们生成java文件,并且是在运行的时候可以访问的。其中使用的技术是apt技术。下面我们一起看看上边的示例生成的代码:


public class ARouter$$Root$$app implements IRouteRoot {
@Override
public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
routes.put("test", ARouter$$Group$$test.class);
}
}

public class ARouter$$Group$$test implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/test/router_activity", RouteMeta.build(RouteType.ACTIVITY, RouterActivity.class, "/test/router_activity", "test", null, -1, -2147483648));
}
}

根据上边生成的代码可以看出,生成的代码就是一张路由表,先将群组跟群组的class对应起来,每个群组里边是该群组下的路由表。


4.初始化init()(加载路由表的群组)


接下来我们看看初始化时,路由框架里边做了哪些事情:


//#ARouter
public static void init(Application application) {
if (!hasInit) {
//...省略部分代码
hasInit = _ARouter.init(application);
//...省略部分代码
}
}

//#_ARouter
protected static synchronized boolean init(Application application) {
mContext = application;
LogisticsCenter.init(mContext, executor);
logger.info(Consts.TAG, "ARouter init success!");
hasInit = true;
mHandler = new Handler(Looper.getMainLooper());
return true;
}

初始化的核心代码看起来就在LogisticsCenter中:


public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
mContext = context;
executor = tpe;

try {
//...省略代码
if (registerByPlugin) {
//...省略代码
} else {
Set<String> routerMap;

// 如果是debug包或者更新版本
if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
//获取在com.alibaba.android.arouter.routes下的所以class类名
routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
//更新到sp中
if (!routerMap.isEmpty()) {
context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply();
}
//更新版本
PackageUtils.updateVersion(context);
} else {
//直接从缓存拿出之前存放的class
routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));
}

//遍历routerMap,将group的类加载到缓存中
for (String className : routerMap) {
if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
//生成的Root、比如我们上面示例的ARouter$$Root$$app,调用loadInto相当于加载了routes.put("test", ARouter$$Group$$test.class)
((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
} else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {
//加载拦截器,例如生成的ARouter$$Interceptors$$app
((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
} else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {
// 加载Provider,例如生成的ARouter$$Providers$$app
((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);
}
}
}
//...省略代码
} catch (Exception e) {
throw new HandlerException(TAG + "ARouter init logistics center exception! [" + e.getMessage() + "]");
}
}

上边的核心逻辑就是如果是debug包或者更新版本,那么就去获取com.alibaba.android.arouter.routes下的所以class类名,然后更新到sp中,并且更新版本号。然后通过反射加载IRouteRoot,去加载群组及对应的class对象,除此还会加载拦截器,Provider。


这里我们重点看一下获取class文件路径的方法getFileNameByPackageName:


public static Set<String> getFileNameByPackageName(Context context, final String packageName) throws NameNotFoundException, IOException, InterruptedException {
final Set<String> classNames = new HashSet();
//获取到dex文件路径
List<String> paths = getSourcePaths(context);
final CountDownLatch parserCtl = new CountDownLatch(paths.size());
Iterator var5 = paths.iterator();

while(var5.hasNext()) {
final String path = (String)var5.next();
DefaultPoolExecutor.getInstance().execute(new Runnable() {
public void run() {
DexFile dexfile = null;
try {
//加载出dexfile文件
if (path.endsWith(".zip")) {
dexfile = DexFile.loadDex(path, path + ".tmp", 0);
} else {
dexfile = new DexFile(path);
}

Enumeration dexEntries = dexfile.entries();
// 遍历dexFile里边的元素,加载出.class文件
while(dexEntries.hasMoreElements()) {
String className = (String)dexEntries.nextElement();
//开头"com.alibaba.android.arouter.routes"
if (className.startsWith(packageName)) {
classNames.add(className);
}
}
} catch (Throwable var12) {
Log.e("ARouter", "Scan map file in dex files made error.", var12);
} finally {
//...省略代码
parserCtl.countDown();
}
}
});
}

parserCtl.await();
//。。。省略代码
return classNames;
}

此方法里边的核心逻辑就是加载出dex文件的路径,然后通过路径构建出DexFile,构建后遍历它里边的元素,如果是com.alibaba.android.arouter.routes开头的class文件,则保存到列表里等待返回。


getSourcePaths:


public static List<String> getSourcePaths(Context context) throws NameNotFoundException, IOException {
ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
File sourceApk = new File(applicationInfo.sourceDir);
List<String> sourcePaths = new ArrayList();
sourcePaths.add(applicationInfo.sourceDir);
String extractedFilePrefix = sourceApk.getName() + ".classes";
//是否开启了multidex,如果开启的话,则需获取每个dex路径
if (!isVMMultidexCapable()) {
int totalDexNumber = getMultiDexPreferences(context).getInt("dex.number", 1);
File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);
//遍历每一个dex文件
for(int secondaryNumber = 2; secondaryNumber <= totalDexNumber; ++secondaryNumber) {
//app.classes2.zip、app.classes3.zip ...
String fileName = extractedFilePrefix + secondaryNumber + ".zip";
File extractedFile = new File(dexDir, fileName);
if (!extractedFile.isFile()) {
throw new IOException("Missing extracted secondary dex file '" + extractedFile.getPath() + "'");
}
sourcePaths.add(extractedFile.getAbsolutePath());
}
}

if (ARouter.debuggable()) {
sourcePaths.addAll(tryLoadInstantRunDexFile(applicationInfo));
}

return sourcePaths;
}

getSourcePaths的功能就是获取app的所有dex文件的路径,为后面转成class文件从而获取class文件路径提供数据。


小结:



  • ARouter.init(this)调用交给了内部的_ARouter.init(application),然后真正做事的是LogisticsCenter.init(mContext, executor)

  • 如果是debug包或者升级版本,则去加载出com.alibaba.android.arouter.routes包下的dex文件的路径,并且更新到缓存里边

  • 通过这些dex去获取对应的所有class文件的路径

  • 最后根据类名的前缀加载到Warehouse中对应的map里,其中就有group、interceptor和provider


5.调用及处理


ARouter.getInstance().build("/test/router_activity").navigation()

build会构建一个Postcard对象出来:


//#Router
public Postcard build(String path) {
return _ARouter.getInstance().build(path);
}

//#_ARouter
protected Postcard build(String path) {
if (TextUtils.isEmpty(path)) {
throw new HandlerException(Consts.TAG + "Parameter is invalid!");
} else {
PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
if (null != pService) {
path = pService.forString(path);
}
//extractGroup方法就是从path中提取出group,比如"/test/router_activity",test便是提取出来的group
return build(path, extractGroup(path));
}
}

build(path, group)方法最终会构建一个Postcard对象出来。


构建好PostCard之后,调用它的navigation方法便可以实现我们的跳转或者获取对应的实体。navigation方法最后会调用到_ARouter的navigation方法:


protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
//...省略代码
try {
//1.根据postCard的group加载路由表,并且补全postCard的信息
LogisticsCenter.completion(postcard);
} catch (NoRouteFoundException ex) {
//...异常处理
return null;
}
if (null != callback) {
callback.onFound(postcard);
}

//如果不是绿色通道的话,需要走拦截器的逻辑,否则会跳过拦截器
if (!postcard.isGreenChannel()) {
interceptorService.doInterceptions(postcard, new InterceptorCallback() {
@Override
public void onContinue(Postcard postcard) {
//2.真正实现动作处理
_navigation(context, postcard, requestCode, callback);
}
@Override
public void onInterrupt(Throwable exception) {
if (null != callback) {
callback.onInterrupt(postcard);
}
//...省略代码
}
});
} else {
//2.真正实现动作处理
return _navigation(context, postcard, requestCode, callback);
}
return null;
}

navigation方法的核心逻辑为:加载路由表,并且补全postCard的信息,然后真正处理跳转或者请求逻辑。


LogisticsCenter.completion(postcard)的核心源码如下:


public synchronized static void completion(Postcard postcard) {
if (null == postcard) {
throw new NoRouteFoundException(TAG + "No postcard!");
}

RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
if (null == routeMeta) {
//groupsIndex在init的时候已经加载好了,这里就可以通过group获取到对应group的class对象
Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup()); // Load route meta.
if (null == groupMeta) {
throw new NoRouteFoundException(TAG + "There is no route match the path [" + postcard.getPath() + "], in group [" + postcard.getGroup() + "]");
} else {
// Load route and cache it into memory, then delete from metas.
try {
//...省略代码
IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
//将group里边的路由表加载进内存,我们最开始的例子想当于执行:atlas.put("/test/router_activity", RouteMeta.build(RouteType.ACTIVITY, RouterActivity.class, "/test/router_activity", "test", null, -1, -2147483648));
iGroupInstance.loadInto(Warehouse.routes);
//因为加载路由表了,所以可以将group从内存中移除,节省内存
Warehouse.groupsIndex.remove(postcard.getGroup());
//...省略代码
} catch (Exception e) {
throw new HandlerException(TAG + "Fatal exception when loading group meta. [" + e.getMessage() + "]");
}
//已经将group里的路由表加载出来了,再执行一遍函数。
completion(postcard); // Reload
}
} else {
// 第二次时,给postCard填补信息
postcard.setDestination(routeMeta.getDestination());
postcard.setType(routeMeta.getType());
postcard.setPriority(routeMeta.getPriority());
postcard.setExtra(routeMeta.getExtra());
//...省略代码,主要是解析uri然后参数的赋值

//根据路由获取的不同的类型,继续补充一些信息给postCard
switch (routeMeta.getType()) {
case PROVIDER:
//...省略代码,主要是补充一些其他参数
postcard.greenChannel(); // Provider should skip all of interceptors
break;
case FRAGMENT:
postcard.greenChannel(); // Fragment needn't interceptors
default:
break;
}
}
}

补充完postCard信息之后,接下来我们看看_navigation方法:


private Object _navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
final Context currentContext = null == context ? mContext : context;

switch (postcard.getType()) {
case ACTIVITY:
//构造Intent,然后切换到主线程,并且跳转到指定的Activity
break;
case PROVIDER:
return postcard.getProvider();
case BOARDCAST:
case CONTENT_PROVIDER:
case FRAGMENT:
//反射构造出实例并返回
case METHOD:
default:
return null;
}

return null;
}

可以看到,最终会根据不同的type,去做出不同的响应,例如ACTIVITY的话,会进行activity的跳转,其他的会通过反射构造出实例返回等操作。


小结:



  • 调用的最开始,会构建一个PostCard对象,初始化path和group

  • navigation方法最终会调用到_ARouter的navigation方法,然后通过LogisticsCenter.completion(postcard)去加载group里边的路由表,并且补全postcard信息。

  • 如果是有绿色通道的话,则不执行拦截器,直接跳过,否则需要执行拦截器。

  • 最后便是通过不同类型执行对应的操作。


结语


本文的分享到这里就结束了,相信看完后,能够对ARouter的原理有了一定的理解,以便我们后面如果有使用到它的时候,能够更好的地使用,或者为项目定制出路由框架提供了很好的思路参考。同时,这么优秀的框架也值得我们去学习它的一些设计思路。


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

为什么说获取堆栈从来就不是一件简单的事情

碎碎谈 为了不让文章看上去过于枯燥,笔者考虑了一下,特意增加了碎碎谈环节!自从上次这篇文章发出去后 黑科技!让Native Crash 与ANR无处发泄!,就挺受读者欢迎的呀,收藏数大于点赞数是什么鬼,嘿嘿!从我的角度出发,本来Signal出发的目的就是想建...
继续阅读 »

碎碎谈


为了不让文章看上去过于枯燥,笔者考虑了一下,特意增加了碎碎谈环节!自从上次这篇文章发出去后 黑科技!让Native Crash 与ANR无处发泄!,就挺受读者欢迎的呀,收藏数大于点赞数是什么鬼,嘿嘿!从我的角度出发,本来Signal出发的目的就是想建造一个类似于安全气囊的装置,保证crash后第一时间重启恢复,达到一个应用稳定的目的,但是慢慢写着写着,发现很多crash监控平台的也是用了相同的核心原理(大部分还没开源噢),只是作用的目标不同,那么为什么不把Signal打造成一个通用的基础件呢!无论是安全气囊还是监控,其实都是上层的应用不同罢了!嗯!有了这个想法之后,给Signal补充一些日志监控逻辑,就更加完善了!所以就有了本篇文章!算是一个补充文!如果没看过黑科技!让Native Crash 与ANR无处发泄!这篇文章的新朋友,请先阅读!(如果没有ndk开发经验也没关系,里面也不涉及很复杂的c知识)


获取堆栈


获取堆栈!可能很多新朋友看到这个就会想,这有什么难的嘛!直接new 一个Throwable获取不就可以了嘛,或者Thread.currentThread().stackTrace(kotlin)等等也可以呀!嗯!是的!我们在java层通常会有很固定的获取堆栈方式,这得益于java虚拟机的设计,也得益于java语言的设计,因为屏蔽了多平台底层的差异,我们就可以用相对统一的api去获取当前的堆栈。这个堆栈也特指java虚拟机堆栈!


但是对于native的堆栈,问题就来了!我们知道native层通常跟很多因素有关,比如链接器,编译器,还有各种库的版本,各种abi等等影响,获取一个堆栈消息,可没有那么简单,因为太多因素干扰了,这也是历史的包袱!还有对于我们android来说,android官方在对堆栈获取的方式,也是有历史变化的


4.1.1以上,5.0以下,android native使用系统自带的libcorkscrew.so,5.0开始,系统中没有了libcorkscrew.so 高版本的安卓源码中就使用了他的优化版替换libunwind。同时对于ndk来说,编译器的版本也在不断变化,从默认的gcc变成clang(ndk >=13),可以看到,我们会在众多版本,众多因素下,找一个统一的方式,还真的不简单!不过呀!在2022的今天,google早已推出了一个计划统一库 breakpad ,嗯!虽然能不能成为标准还未定,但是也是一个生态的进步


Signal的选择


前面介绍了这么多方案,breakpad是不是Signal的首选呢!虽然breakpad不错,但是里面覆盖了太多其他系统的编译,比如mac,window等等标准,还有就是作为一个开源库,还是希望减少这些库的导入,所以跟大多数主流方案一直,我们选择用unwind.h去实现堆栈打印,因为这个就直接内置在我们的默认编译中了,而且这个在在android也能用!下面我们来看一下实现!即Signal项目的unwind-utils的实现。那么我们要考虑一些什么呢!


堆栈大小


日志当然需要设定追溯的堆栈大小,内容太多不好(过于臃肿,排查困难),内容太少也不好(很有可能漏掉关键crash堆栈),所以Signal默认设置30条,可以根据实际项目修改


std::string backtraceToLogcat() {
默认30个
const size_t max = 30;
void *buffer[max];
//ostringstream方便输出string
std::ostringstream oss;
dumpBacktrace(oss, buffer, captureBacktrace(buffer, max));
return oss.str();
}

_Unwind_Backtrace


_Unwind_Backtrace是unwind提供给我们堆栈回溯函数


_Unwind_Reason_Code _Unwind_Backtrace(_Unwind_Trace_Fn, void *);

那么这个_Unwind_Trace_Fn是个啥,其实点进去看


typedef _Unwind_Reason_Code (*_Unwind_Trace_Fn)(struct _Unwind_Context *,
void *);

其实这就代表一个函数,对于我们常年写java的朋友有点不友好对吧,以java的方式,其实意思就是传xxx(随便函数名)( _Unwind_Context *,void *)这样的结构的函数即可,这里的意思就是一个callback函数,当我们获取到地址信息就会回调该参数,第二个就是需要传递给参数一的参数,这里有点绕对吧,我们怎么理解呢!参数一其实就是一个函数的引用,那么这个函数需要参数怎么办,就通过第二个参数传递!


我们看个例子:这个在Signal也有


static _Unwind_Reason_Code unwindCallback(struct _Unwind_Context *context, void *args) {
BacktraceState *state = static_cast<BacktraceState *>(args);
uintptr_t pc = _Unwind_GetIP(context);
if (pc) {
if (state->current == state->end) {
return _URC_END_OF_STACK;
} else {
*state->current++ = reinterpret_cast<void *>(pc);
}
}
return _URC_NO_REASON;
}


size_t captureBacktrace(void **buffer, size_t max) {
BacktraceState state = {buffer, buffer + max};
_Unwind_Backtrace(unwindCallback, &state);
// 获取大小
return state.current - buffer;
}

struct BacktraceState {
void **current;
void **end;
};

我们定义了一个结构体BacktraceState,其实是为了后面记录函数地址而用,这里有两个作用,end代表日志限定的大小,current表示实际日志条数大小(因为堆栈条数可能小于end)


_Unwind_GetIP


我们在unwindCallback这里拿到了系统回调给我们的参数,关键就是这个了 _Unwind_Context这个结构体参数了,这个参数的作用就是传递给_Unwind_GetIP这个函数,获取我们当前的执行地址,即pc值!那么这个pc值又有什么用呢!这个就是我们获取堆栈的关键!native堆栈的获取需要地址去解析!(不同于java)我们先有这个概念,后面会继续讲解


dladdr


经过了_Unwind_GetIP我们获取了pc值,这个时候就用上dladdr函数去解析了,这个是linux内核函数,专门用于地址符号解析


The function dladdr() determines whether the address specified in
addr is located in one of the shared objects loaded by the
calling application. If it is, then dladdr() returns information
about the shared object and symbol that overlaps addr. This
information is returned in a Dl_info structure:

typedef struct {
const char *dli_fname; /* Pathname of shared object that
contains address */
void *dli_fbase; /* Base address at which shared
object is loaded */
const char *dli_sname; /* Name of symbol whose definition
overlaps addr */
void *dli_saddr; /* Exact address of symbol named
in dli_sname */
} Dl_info;

If no symbol matching addr could be found, then dli_sname and
dli_saddr are set to NULL.

可以看到,每个地址会的解析信息会保存在Dl_info中,如果有运行符号满足,dli_sname和dli_saddr就会被设定为相应的so名称跟地址,dli_fbase是基址信息,因为我们的so库被加载到程序的位置是不固定的!所以一般采用地址偏移的方式去在运行时寻找真正的so库,所以就有这个dli_fbase信息。


Dl_info info;
if (dladdr(addr, &info) && info.dli_sname) {
symbol = info.dli_sname;

}
os << " #" << idx << ": " << addr << " " <<" "<<symbol <<"\n" ;

最终我们可以通过dladdr,一一把保存的地址信息解析出来,打印到native日志中比如Signal中demo crash信息(如果需要打印so名称,也可以通过dli_fname去获取,这里不举例)


image.png


native堆栈产生过程


通过上面的日志分析(最好看下demo中的app演示crash),我们其实在MainActivity中设定了一个crash函数


private external fun throwNativeCrash()

按照堆栈日志分析来看,只有在第16条才出现了调用符号,这跟我们在日常java开发中是不是很不一样!因为java层的堆栈一般都是最近的堆栈消息代表着错误消息,比如应该是第0条才导致的crash,但是演示中真正的堆栈crash却隐藏在了日志海里面!相信有不少朋友在看native crash日志也是,是不是也感到无从下手,因为首条日志往往并不是真正crash的主因!我们来看一下真正的过程:我们程序从正常态到crash,究竟发生了什么!


image.png


可以看到,我们真正dump_stack前,是有很多前置的步骤,为什么会有这么多呢!其实这就涉及到linux内核中断的原理,这里给一张粗略图


image.png
crash产生后,一般会在用户态阶段调用中断进入内核态,把自己的中断信号(这里区分一下,不是我们signal.h里面的信号)放在eax寄存器中(大部分,也有其他的寄存器,这里仅举例)


然后内核层通过传来的中断信号,找到信号表,然后根据对应的处理程序,再抛回给用户态,这个时候才进行sigaction的逻辑


所以说,crash产生到真正dump日志,其实会有一个过程,这里面根据sigaction的设置也会有多个变化,我们要了解的一点是,真正的crash信息,往往藏在堆栈海中,需要我们一步步去解析,比如通过addr2line等工具去分析地址,才能得到真正的原因,而且一般的android项目,都是依赖于第三方的so,这也给我们的排查带来难度,不过只要我们能识别出特定的so(dli_fname信息就有),是不是就可以把锅甩出去了呢,对吧!


最后


看到这里,读者朋友应该有一个对native堆栈的大概模型了,当然也不用怕!Signal项目中就包含了相关的unwind-utils工具类,直接用也是可以的,不过目前打印的信息比较简单,后续可以根据大家的实际,去添加参数!代码都在里面,求star求pr !Signal,当然,看完了本文,别忘了留下你的赞跟评论呀!


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

Kotlin 快速编译背后的黑科技,了解一下~

前言 快速编译大量代码一向是一个难题,尤其是当编译器必须执行很多复杂操作时,例如重载方法解析和泛型类型推断。 本文主要介绍在日常开发中做一些小改动时,Kotlin编译器是如何加快编译速度的 为什么编译那么耗时? 编译时间长通常有三大原因: 代码库大小:通常代...
继续阅读 »

前言


快速编译大量代码一向是一个难题,尤其是当编译器必须执行很多复杂操作时,例如重载方法解析和泛型类型推断。 本文主要介绍在日常开发中做一些小改动时,Kotlin编译器是如何加快编译速度的


为什么编译那么耗时?


编译时间长通常有三大原因:



  1. 代码库大小:通常代码码越大,编译耗时越长

  2. 你的工具链优化了多少,这包括编译器本身和你正在使用的任何构建工具。

  3. 你的编译器有多智能:无论是在不打扰用户的情况下计算出许多事情,还是需要不断提示和样板代码


前两个因素很明显,让我们谈谈第三个因素:编译器的智能。 这通常是一个复杂的权衡,在 Kotlin 中,我们决定支持干净可读的类型安全代码。这意味着编译器必须非常智能,因为我们在编译期需要做很多工作。


Kotlin 旨在用于项目寿命长、规模大且涉及大量人员的工业开发环境。


因此,我们希望静态类型安全,能够及早发现错误,并获得精确的提示(支持自动补全、重构和在 IDE 中查找使用、精确的代码导航等)。


然后,我们还想要干净可读的代码,没有不必要的噪音。这意味着我们不希望代码中到处都是类型。 这就是为什么我们有支持 lambda 和扩展函数类型的智能类型推断和重载解析算法等等。 Kotlin 编译器会自己计算出很多东西,以同时保持代码干净和类型安全。


编译器可以同时智能与高效吗?


为了让智能编译器快速运行,您当然需要优化工具链的每一部分,这是我们一直在努力的事情。 除此之外,我们正在开发新一代 Kotlin 编译器,它的运行速度将比当前编译器快得多,但这篇文章不是关于这个的。


不管编译器有多快,在大型项目上都不会太快。 而且,在调试时所做的每一个小改动都重新编译整个代码库是一种巨大的浪费。 因此,我们试图尽可能多地复用之前的编译,并且只编译我们绝对需要的文件。


有两种通用方法可以减少重新编译的代码量:



  • 编译避免:即只重新编译受影响的模块,

  • 增量编译:即只重新编译受影响的文件。


人们可能会想到一种更细粒度的方法,它可以跟踪单个函数或类的变化,因此重新编译的次数甚至少于一个文件,但我不知道这种方法在工业语言中的实际实现,总的来说它似乎没有必要。


现在让我们更详细地了解一下编译避免和增量编译。


编译避免


编译避免的核心思想是:



  • 查找dirty(即发生更改)的文件

  • 重新编译这些文件所属的module

  • 确定哪些其他模块可能会受到更改的影响,重新编译这些模块,并检查它们的ABI

  • 然后重复这个过程直到重新编译所有受影响的模块


从以上步骤可以看出,没有人依赖的模块中的更改将比每个人都依赖的模块(比如util模块)中的更改编译得更快(如果它影响其 ABI),因为如果你修改了util模块,依赖了它的模块全都需要编译


ABI是什么


上面介绍了在编译过程中会检查ABI,那么ABI是什么呢?


ABI 代表应用程序二进制接口,它与 API 相同,但用于二进制文件。本质上,ABI 是依赖模块关心的二进制文件中唯一的部分。


粗略地说,Kotlin 二进制文件(无论是 JVM 类文件还是 KLib)包含declarationbody两部分。其他模块可以引用declaration,但不是所有declaration。因此,例如,私有类和成员不是 ABI 的一部分。


body可以成为 ABI 的一部分吗?也是可以的,比如当我们使用inline时。 同时Kotlin 具有内联函数和编译时常量(const val)。因此如果内联函数的bodyconst val 的值发生更改,则可能需要重新编译相关模块。


因此,粗略地说,Kotlin 模块的 ABIdeclaration、内联body和其他模块可见的const val值组成。


因此检测 ABI 变化的直接方法是



  • 以某种形式存储先前编译的 ABI(您可能希望存储哈希以提高效率)

  • 编译模块后,将结果与存储的 ABI 进行比较:

  • 如果相同,我们就完成了;

  • 如果改变了,重新编译依赖模块。


编译避免的优缺点


避免编译的最大优点是相对简单。


当模块很小时,这种方法确实很有帮助,因为重新编译的单元是整个模块。 但如果你的模块很大,重新编译的耗时会很长。 因此为了尽可能地利用编译避免提升速度,决定了我们的工程应该由很多小模块组成。作为开发人员,我们可能想要也可能不想要这个。 小模块不一定听起来像一个糟糕的设计,但我宁愿为人而不是机器构建我的代码。为了利用编译避免,实际上限制了我们项目的架构。


另一个观察结果是,许多项目都有类似于util的基础模块,其中包含许多有用的小功能。 几乎所有其他模块都依赖于util模块,至少是可传递的。 现在,假设我想添加另一个在我的代码库中使用了 3 次的小实用函数。 它添加到util模块中会导致ABI发生变化,因此所有依赖模块都受到影响,进而导致整个项目都需要重新编译。


最重要的是,拥有许多小模块(每个都依赖于多个其他模块)意味着我的项目的configuration时间可能会变得巨大,因为对于每个模块,它都包含其独特的依赖项集(源代码和二进制文件)。 在 Gradle 中配置每个模块通常需要 50-100 毫秒。 大型项目拥有超过 1000 个模块的情况并不少见,因此总配置时间可能会超过一分钟。 它必须在每次构建以及每次将项目导入 IDE 时都运行(例如,添加新依赖项时)。


Gradle 中有许多特性可以减轻编译避免的一些缺点:例如,可以使用缓存configuration cache。 尽管如此,这里仍有很大的改进空间,这就是为什么在 Kotlin 中我们使用增量编译。


增量编译


增量编译比编译避免更加细粒度:它适用于单个文件而不是模块。 因此,当通用模块的 ABI 发生微小变化时,它不关心模块大小,也不重新编译整个项目。这种方式不会限制用户项目的架构,并且可以加快编译速度


JPS(IntelliJ的内置构建系统)一直支持增量编译。 而Gradle仅支持开箱即用的编译避免。 从 1.4 开始,Kotlin Gradle 插件为 Gradle 带来了一些有限的增量编译实现,但仍有很大的改进空间。


理想情况下,我们只需查看更改的文件,准确确定哪些文件依赖于它们,然后重新编译所有这些文件。


听起来很简单,但实际上准确地确定这组依赖文件非常复杂。


一方面,源文件之间可能存在循环依赖关系,这是大多数现代构建系统中的模块所不允许的。并且单个文件的依赖关系没有明确声明。请注意,如果引用了相同的包和链调用,imports不足以确定依赖关系:对于 A.b.c(),我们最多需要导入 A,但 B 类型的更改也会影响我们。


由于所有这些复杂性,增量编译试图通过多轮来获取受影响的文件集,以下是它的完成方式的概要:



  • 查找dirty(更改)的文件

  • 重新编译它们(使用之前编译的结果作为二进制依赖,而不是编译其他源文件)

  • 检查这些文件对应的ABI是否发生了变化

  • 如果没有,我们就完成了!

  • 如果发生了变化,则查找受更改影响的文件,将它们添加到脏文件集中,重新编译

  • 重复直到 ABI 稳定(这称为“固定点”)


由于我们已经知道如何比较 ABI,所以这里基本上只有两个棘手的地方:



  • 使用先前编译的结果来编译源的任意子集

  • 查找受一组给定的 ABI 更改影响的文件。


这两者都是 Kotlin 增量编译器的功能。 让我们一个一个看一下。


编译脏文件


编译器知道如何使用先前编译结果的子集来跳过编译非脏文件,而只需加载其中定义的符号来为脏文件生成二进制文件。 如果不是为了增量,编译器不一定能够做到这一点:从模块生成一个大二进制文件而不是每个源文件生成一个小二进制文件,这在 JVM 世界之外并不常见。 而且它不是 Kotlin 语言的一个特性,它是增量编译器的一个实现细节。


当我们将脏文件的 ABI 与之前的结果进行比较时,我们可能会发现我们很幸运,不需要再进行几轮重新编译。 以下是一些只需要重新编译脏文件的更改示例(因为它们不会更改 ABI):



  • 注释、字符串文字(const val 除外)等,例如:更改调试输出中的某些内容

  • 更改仅限于非内联且不影响返回类型推断的函数体,例如:添加/删除调试输出,或更改函数的内部逻辑

  • 仅限于私有声明的更改(它们可以是类或文件私有的),例如:引入或重命名私有函数

  • 重新排序函数声明


如您所见,这些情况在调试和迭代改进代码时非常常见。


扩大脏文件集


如果我们不那么幸运并且某些声明已更改,则意味着某些依赖于脏文件的文件在重新编译时可能会产生不同的结果,即使它们的代码中没有任何一行被更改。


一个简单的策略是此时放弃并重新编译整个模块。

这将把所有编译避免的问题都摆在桌面上:一旦你修改了一个声明,大模块就会成为一个问题,而且大量的小模块也有性能成本,如上所述。

所以,我们需要更细化:找到受影响的文件并重新编译它们。


因此,我们希望找到依赖于实际更改的 ABI 部分的文件。

例如,如果用户将 foo 重命名为 bar,我们只想重新编译关心名称 foobar 的文件,而不管其他文件,即使它们引用了此 ABI的其他部分。

增量编译器会记住哪些文件依赖于先前编译中的哪个声明,我们可以使用这种数据,有点像模块依赖图。同样,这不是非增量编译器通常会做的事情。


理想情况下,对于每个文件,我们应该存储哪些文件依赖于它,以及它们关心 ABI 的哪些部分。实际上,如此精确地存储所有依赖项的成本太高了。而且在许多情况下,存储完整签名毫无意义。


我们看一下下面这个例子:


// dirty.kt
// rename this to be 'fun foo(i: Int)'
fun changeMe(i: Int) = if (i == 1) 0 else bar().length

// clean.kt
fun foo(a: Any) = ""
fun bar() = foo(1)

我们定义两个kt文件 ,dirty.ktclean.kt


假设用户将函数 changeMe 重命名为 foo。 请注意,虽然 clean.kt 没有改变,但 bar() 的主体将在重新编译时改变:它现在将从dirty.kt 调用 foo(Int),而不是从 clean.kt 调用 foo(Any) ,并且它的返回类型 也会改变。


这意味着我们必须重新编译dirty.ktclean.kt。 增量编译器如何发现这一点?


我们首先重新编译更改的文件:dirty.kt。 然后我们看到 ABI 中的某些内容发生了变化:



  • 没有功能 changeMe

  • 有一个函数 foo 接受一个 Int 并返回一个 Int


现在我们看到 clean.kt 依赖于名称 foo。 这意味着我们必须再次重新编译 clean.ktdirty.kt。 为什么? 因为类型不能被信任。


增量编译必须产生与所有代码的完全重新编译相同的结果。

考虑dirty.kt 中新出现的foo 的返回类型。它是推断出来的,实际上它取决于 clean.ktbar 的类型,它是文件之间的循环依赖。

因此,当我们将 clean.kt 添加到组合中时,返回类型可能会发生变化。在这个例子中,我们会得到一个编译错误,但是在我们重新编译 clean.ktdirty.kt 之前,我们不知道它。


Kotlin 增量编译的第一原则:您可以信任的只是名称。


这就是为什么对于每个文件,我们存储它产生的 ABI,以及在编译期间查找的名称(不是完整的声明)。


我们存储所有这些的方式可以进行一些优化。


例如,某些名称永远不会在文件之外查找,例如局部变量的名称,在某些情况下还有局部函数的名称。

我们可以从索引中省略它们。为了使算法更精确,我们记录了在查找每个名称时查阅了哪些文件。为了压缩我们使用散列的索引。这里有更多改进的空间。


您可能已经注意到,我们必须多次重新编译初始的脏文件集。 唉,没有办法解决这个问题:可能存在循环依赖,只有一次编译所有受影响的文件才能产生正确的结果。


在最坏的情况下,增量编译可能会比编译避免做更多的工作,因此应该有适当的启发式方法来防止它。


跨模块的增量编译


迄今为止最大的挑战是可以跨越模块边界的增量编译。


比如说,我们在一个模块中有脏文件,我们做了几轮并在那里到达一个固定点。现在我们有了这个模块的新 ABI,需要对依赖的模块做一些事情。


当然,我们知道初始模块的 ABI 中哪些名称受到影响,并且我们知道依赖模块中的哪些文件查找了这些名称。


现在,我们可以应用基本相同的增量算法,但从 ABI 更改开始,而不是从一组脏文件开始。


如果模块之间没有循环依赖,单独重新编译依赖文件就足够了。但是,如果他们的 ABI 发生了变化,我们需要将更多来自同一模块的文件添加到集合中,并再次重新编译相同的文件。


Gradle 中完全实现这一点是一个公开的挑战。这可能需要对 Gradle 架构进行一些更改,但我们从过去的经验中知道,这样的事情是可能的,并且受到 Gradle 团队的欢迎。


总结


现在,您对现代编程语言中的快速编译所带来的挑战有了基本的了解。请注意,一些语言故意选择让他们的编译器不那么智能,以避免不得不做这一切。不管好坏,Kotlin 走的是另一条路,让 Kotlin 编译器如此智能似乎是用户最喜欢的特性,因为它们同时提供了强大的抽象、可读性和简洁的代码。


虽然我们正在开发新一代编译器前端,它将通过重新考虑核心类型检查和名称解析算法的实现来加快编译速度,但我们知道这篇博文中描述的所有内容都不会过时。


原因之一是使用 Java 编程语言的体验,它享受 IntelliJ IDEA 的增量编译功能,甚至拥有比今天的 kotlinc 快得多的编译器。


另一个原因是我们的目标是尽可能接近解释语言的开发体验,这些语言无需任何编译即可立即获取更改。


所以,Kotlin 的快速编译策略是:优化的编译器 + 优化的工具链 + 复杂的增量。


译者总结


本文主要介绍了Kotlin编译器在加快编译速度方面做的一些工作,介绍了编译避免与增量编译的区别以及什么是ABI


了解Kotlin增量编译的原理可以帮助我们提高增量编译成功的概率,比如inline函数体也是ABI的一部分,因此当我们声明内联函数时,内联函数体应该写得尽量简单,内部通常只需要调用另一个非内联函数即可。


这样当inline函数内部逻辑发生更改时,不需要重新编译依赖于它的那些文件,从而实现增量编译。


同时从实际开发过程中体验,Kotlin增量编译还是经常会失效,尤其是发生跨模块更改时。Kotlin新一代编译器已经发布了Alpha版本,期待会有更好的表现~


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

2022年各国开发者薪资水平报告:中国排19位,第1位是中国的5倍

来源:oschina.net/news/202254/software-engineer-salary-by-country-2022智能招聘平台 CodeSubmit 统计分析了 20 多个国家在 2022 年的软件工程领域的平均工资发现,美国的软件工程师薪...
继续阅读 »

来源:oschina.net/news/202254/software-engineer-salary-by-country-2022

智能招聘平台 CodeSubmit 统计分析了 20 多个国家在 2022 年的软件工程领域的平均工资发现,美国的软件工程师薪资水平最高,瑞士和以色列紧随其后。前十榜单还包括丹麦、加拿大、挪威、澳大利亚、英国、德国和瑞典。中国则排在第 19 顺位,平均薪资水平为 23,790 美元 / 年。



其他方面,印度是一个西方公司倾向于将其技术需求外包的国家,其平均年薪为 7,725 美元。尼日利亚薪资水平垫底,为 7,255 美元 / 年。CodeSubmit 方面指出,总体而言软件开发是世界上最受欢迎的职业。对软件开发人员需求最高的国家是加拿大、澳大利亚、俄罗斯、瑞典和新西兰;由于人才紧缺,工资水平也往往更高。

美国软件开发人员的平均工资为每年 110,140 美元或每月 9,178 美元。初级开发人员的平均工资为每年 69,354 美元或每月 5,779 美元,高级开发人员的平均工资为每年 104,188 美元或每月 8,682 美元。软件开发人员收入最高的州是加利福尼亚,平均工资为 146,770 美元;华盛顿次之。收入最高的城市包括圣何塞(167,420 美元)、旧金山(158,320 美元)和西雅图(148,200 美元)。

该国的编程语言平均薪资水平中,Go 和 Python 等流行的后端语言位居榜首。具体表现为:

  • Go 是收入最高的语言,120,577 美元。
  • Ruby 以 119,558 美元位居第二
  • Python 为 114,904 美元
  • Java 的平均工资为 112,013 美元
  • JavaScript 为 111,922 美元
  • Android 开发者的平均收入为 109,377 美元
  • 与 Android 相比,iOS 的平均工资略低,为 108,783 美元
  • Rust 紧随其后,为 108,744 美元
  • C 语言 101,734 美元
  • PHP 为 92,867 美元
  • SQL 最少为 85,845 美元

欧洲软件开发人员的平均工资水平低于美国。总体而言,欧洲国家在东西方之间存在很大差异。西欧开发者的年收入至少为 40,000 美元以上,而东欧的开发者期望的收入要少得多,约为 20,000 美元以上;南欧开发者的薪酬也要低于北欧开发者。西班牙、意大利、葡萄牙和希腊的开发人员预计年薪范围在 21,314 到 36,323 美元。

美国和欧洲国家之外,以色列软件开发人员的平均年薪为 71,559 美元或每月 5,963 美元。初级开发人员每年赚 69,851 美元或每月 5,820 美元,高级开发人员通常年薪为 114,751 美元或每月 9,562 美元。

语言方面,Golang(每年 109,702 美元)和 Python(每年 83,369 美元)平均薪资水平最高。PHP 和 Ruby 在以色列支付的薪资水平最低,分别为每年 64,573 美元和 64,525 美元。

  • Golang 开发人员的年平均收入为 109,702 美元。
  • Python 开发人员的收入为 83,369 美元。
  • 移动开发者的薪酬排名第三和第四:Android 开发者的年薪为 78,558 美元,iOS 开发者的年薪为 76,692 美元。
  • Java 开发人员的薪酬为 74,251 美元。
  • JavaScript 开发人员的收入为 72,028 美元。
  • SQL 开发人员在以色列的薪酬为 65,770 美元。
  • PHP(64,573 美元)和 Ruby(64,525 美元)是以色列收入最低的语言。

此外,日本开发人员的平均工资为每年 36,024 美元或每月 3,002 美元。编程语言薪资方面,iOS 水平最高,Ruby 位居第二;SQL 和 Java 是日本收入最低的编程语言。印度软件开发人员的平均工资为每年 7,725 美元或每月 643 美元。Ruby 是印度收入最高的编程语言,每年 12,372 美元。Android 是薪资水平最低的语言,为 5,181 美元 / 年。

总体而言,各国总体编程语言薪资水平中,Golang 和 Ruby 往往是高薪语言,而 JavaScript 和 PHP 则是工资最低的语言。


完整数据:https://codesubmit.io/blog/software-engineer-salary-by-country/

来源:oschina.net/news/202254/software-engineer-salary-by-country-2022

收起阅读 »

笑哭了!日本网友求助如何卸载360浏览器,过程堪比“ 拆弹 ”...

360全家桶,相信是每个人都会经历且最后不会再继续用的一款软件吧。从垃圾软件杀手到垃圾软件之王,我们一起见证了它的成长。本以为这个知名软件就在国内火,没想到居然在国外也火了,但并不是因为软件本身有多牛x,而是一名日本网友想要卸载,但一路坎坷,过程堪比“拆弹”。...
继续阅读 »

360全家桶,相信是每个人都会经历且最后不会再继续用的一款软件吧。从垃圾软件杀手到垃圾软件之王,我们一起见证了它的成长。本以为这个知名软件就在国内火,没想到居然在国外也火了,但并不是因为软件本身有多牛x,而是一名日本网友想要卸载,但一路坎坷,过程堪比“拆弹”。下面一起来看看这个经过吧!

就在前段时间,推特上一位日本网友发的帖子,在国内火了。这事说来也挺搞笑,因为——他不会卸载360安全浏览器。

网友发帖求助

说实话,这个弹窗中的卸载按钮,也太难找了。好在评论区里出现了懂汉语的老哥,提醒他选择「忍痛卸载」。师爷,您给翻译翻译,什么叫卸载 ▼

网友回复

但这位日本网友显然还是太年轻,接下来又遇到了新的问题。“ 好的,那我接下来该怎么办?”,他对着下一个页面继续发呆。热心老哥也耐心回复:“ 点右下角那个继续卸载 ”。

网友发帖求助

本以为终于结束了,谁成想360又杀出一记回马枪,来了一手卸载问卷调查。每个选项里还用星号标记,显示必填。

网友发帖求助

不得不说,360还真是个逆向思维大师,藏东西有一手。它推荐你点的按钮,比如加粗加大的字体,没有一个是用来卸载的,反而一个手滑就能给你安装上最新版。真正的卸载按钮,则藏得比私房钱都难找

别说看不懂中文的外国人了,即使熟知这种套路的我们,也相当容易翻车。而且,评论区还有另一位过来人表示:“这东西你卸载不干净的,它很可能会卷土重来。”

网友回复

随后,闻讯而来的国内网友,也立马加入了支招大军。提醒博主卸载后也要留个心眼,因为“请不要在下面取消你不想安装的附加软件”这里,曾让三个语文老师口吐白沫。万一不小心再安装个贪玩蓝月,里面的人不但不会帮你卸载,还会让你拿刀砍他。并表示,360这玩意我们自己都不用,你下载它是为了啥?

网友回复

而这位日本网友的目的,其实也很简单。他只是想在4399上体验一款小游戏,听说用360浏览器才能畅玩。后来听说自己在中国火了,微博贴吧都在讨论这事,表示有点震惊,又有点开心。

网友发帖求助

到此,整件事终于告一段落,世超看完觉得有点气,又有点好笑。因为外国人想卸载国内软件,却一不小搞了个全家桶的事,属实不少。比如YouTube上,你甚至能找到一堆,专门「教你如何卸载360安全卫士的教学视频

在这类视频评论区下方,各国人民齐聚一堂,他们虽然有着不同的语言,却有着同一个梦想——卸载360安全卫士。也因为按照视频步骤,成功卸载了360,他们脸上终于露出了久违的笑容。

世超不知道,有多少个国家的人被360“制裁”过。但视频评论区里不止出现过英语、葡萄牙语、西班牙语、阿拉伯语、韩语等语言,再凑凑应该又够开一波奥运会。

网友评论

很少有人是单纯地想给自己电脑杀毒,他们目标不同,只是机缘巧合之下,最后都同时安装了360软件。比如有位美国网友,本来只是在用360 WIFI,但不知道点到了哪,最后直接被安排上了一套完整的360豪华套餐。还有的人是把电脑借给自己叔叔,拿回来时桌面图标都快被占满了。

网友评论

虽然对于咱们来说,这些套路已经见怪不怪了。但估计谁也讲不清楚,国内电脑软件到底什么时候玩起「卸载伤痛文学」的。每每世超要卸载一个软件,都像是要辜负一个深爱着我的女孩。重要的是,她还一片痴心,并对我报以最诚挚的祝福,我还真有点下不去手。说出来你可能不信,我跟浏览器网恋了 ▼

而与堪比“ 拆弹 ”的卸载过程相比,更麻烦的还是软件删不干净。比如另一个我们熟悉的老朋友——2345,无论国内外都臭名昭著。和它相关的软件,称得上是互联网时代的狗皮膏药。

也许你开始只是想玩玩小游戏,或想随便找一个格式转换器什么的,误打误撞运行了一个 p2p 下载器,这之后噩梦便开始了。默认浏览器被篡改,右下角开始出现弹窗,油腻的师姐带你冒险,上古鲲鹏打起了篮球,成龙大哥也在沙场准时等你。虽然你拼命卸载、粉碎文件,但它的再生能力胜过魔人布欧,隔三差五就会跳出来向你挑衅一番。甚至你为了卸载这个电脑管家,又安装了好几个电脑管家,最后惊醒自己原来是在养蛊。

因为无论如何都删不净,被2345气到的网友义愤填膺,差点冲动行事,打算来一手擒贼先擒王。

网友评论

最后,实在没办法,只好颤抖地点上一支烟,打开一则视频——“ 3分钟教你重装系统,奶奶看了都学会 ”。然后心如止水,等待电脑重启,这都算是老网民们共同的过去了。

可能有的差友会觉得——“ 国人警惕性已经提高了,任凭这些流氓软件作去吧,反正都要被时代淘汰了,现在也就暂时欺负欺负不懂中文、又单纯的老外了 ”。然而,事实并非如此,这些软件过得可比我们想象中滋润多了。不但早早就上了市,并顺应时代搞了一款互联网借贷,2021年还转亏为盈,赚的盆满钵满。

更讽刺的是,前段时间2345还和金山毒霸打了个官司。原因是金山毒霸利用技术手段,将用户的「2345网址导航」替换成了「毒霸网址导航」,最后金山还被判罚了250万。这个事,叫什么喊捉什么来着?

而且流氓软件从未失去自己的市场,它们仍像小广告一般贴在网上各个角落。一旦趁虚而入你的电脑,就算卸载了也会残留文件和注册表项,以便卷土重来。有的则会将自己并入系统进程,增加内存负担。强行删除的话,会反复跳出“ 该程序处于占用中 ”。彻底结束流氓进程,还有可能导致Win崩溃,最后只好重装系统。

再加上,它们还在用着邪道的全家桶策略。本来只下一个浏览器,结果又给你装上了 XX 看图王、XX 手机助手、XX 管家卫士。打开图片会默认启动,连接手机也会自动跳出,甚至能跟你混个脸熟。

其实,如今干净又卫生的软件不少。比如 Chrome 浏览器、Win 系统自带的 Windows Security 和照片管理、火绒安全。但还是有人屡屡中招,这又是为啥?

因为,现在的流氓软件,恰恰就是利用了信息差做买卖。尽管有些软件确实好用,不过依然有很多人无从了解,这点在国内也好,国外也罢。我们能在网上看到的人,确实都在控诉流氓软件的罪过。而没发声的人里,又有多少人还在默默忍受流氓的骚扰呢?这就无从而知了。。。

天下苦流氓软件久矣。以至于,现在世超卸载一款国产软件时,如果它消失得很干脆,我甚至有种离奇的失落感。居然没和我勾心斗角?也没有捆绑下载?它真的,我哭死,居然可以一键卸载,好想把它再下载回来啊 ( 就是这么错乱)!

来源:c.m.163.com/news/a/H336IVP80526D8LR.html

收起阅读 »

易洋千玺也考公,宇宙尽头是编制?

经常有读者问:做程序员太累要不要考个公务员或者找一份国企工作、刚毕业是选择考公还是去互联网公司?这些问题不同人有不同的答案,很多时候没法回答,只好回一句:要不先考上公务员或者拿了国企Offer再做选择?现在考公和进国企(有编制的那种)可真不简单,用难如登天夸张...
继续阅读 »

经常有读者问:做程序员太累要不要考个公务员或者找一份国企工作、刚毕业是选择考公还是去互联网公司?


这些问题不同人有不同的答案,很多时候没法回答,只好回一句:要不先考上公务员或者拿了国企Offer再做选择?


现在考公和进国企(有编制的那种)可真不简单,用难如登天夸张了点,但绝对要耗费很多努力和精力。


这不最近易洋千玺都进话剧院了,成了有编制的人。这个事一下就上热搜了,一堆人各种喷。


很多人质疑为啥没公开三次面试的情况、为啥能以应届生身份考上编制,以及易洋千玺在入选后注销公司是不是违规的问题。


大家质疑的原因也很简单:今年就业多困难啊,高校毕业生目前签约率不足30%,你这么一个大明显还要来跟大家抢饭碗,还抢的是普通人眼里的“金饭碗”。抢就算了,还不需要笔试直接面试就行了。


关于明星背后的故事洋哥不想多探讨,这篇重点想聊聊工作选择的问题。


很多行业都有类似“康波周期”的因素在驱动,今天你看着是“金饭碗”,明天搞不好就成了鸡肋。


我本科毕业是2006年,那个年代最好的工作选择是什么?是西门子、北电为代表的通讯外企。


依稀记得考完研究生后我参加过北电在武汉的招聘,地点是香格里拉大酒店,跟师兄们打探的薪资简直惊呆了我幼小的心灵,最后面试没过,只好乖乖读研去了。

两年后研究生毕业,最好的Offer又发生了一些变化。2008年,已经有各种传闻通讯行业不行了,北电可能都要倒闭了,这时期最受学子们欢迎的是软件外企,比如微软、IBM等。


考公和国企在那个年代只能排在外企屈居第二


毕业四年后,我担任360武汉校招的面试官,这次经历让我发现互联网企业已经成了最受学子们欢迎的选项。


前几年就更不用说了,大厂工牌都成了一个梗,尤其是某条工牌,仿佛拥有了它就拥有了全世界。


变化总是一刻不停的悄悄发生着,你看看,今天的外企是什么样子,你再看看这才两年时间大厂工牌马上就要成为过去时,现在大家开始说:宇宙的尽头是编制。


其实狗屁尽头啊,无非是最近两年经济不行了,大家开始求稳躺平而已。


问题是这种跟着潮流的选择真的能对吗?


拉长时间线来看,这种选择模式大概率不会对。你追求外企的高薪,却忽略了自己是个渴望主动权的人,不愿意做边角料的工作、你追求公务员的稳定,却没想到自己耐不住低工资,更抵挡不住老婆的嘀咕、你追求财务自由选择了互联网公司,却扛不住996的痛苦和摧残。


抛开爱好和心性做出来的从众选择,很难正确。这里拿我自己举个栗子吧:


旧文也说过我毕业的时候面临了好几个选择:外企(autodesk)、互联网民企(腾讯)、国企(国家开发银行)。


如果从众应该选择外企或者国企,但最后我选择的是腾讯。


原因很简单,我喜欢玩游戏,渴望去腾讯做出顶级的游戏。并且在研究生期间也学习了大量的游戏开发的知识。


因为这个选择,被父母唠叨了好多年,在他们看来只有公务员或国企才算正儿八经的工作。民企?那不是黑工厂吗?


但也是因为做了有目标的选择,所以才能扛下来在腾讯、在360的巨大压力。


当然我也是幸运的,因为自己的爱好,撞到了互联网的黄金10年,但即便没有吃到任何红利我也不会后悔自己的选择。


前段时间跟一个在国企的老同学约饭,他挺眉飞色舞的说:“幸好毕业选择了国企,虽然过去痛苦了好多年,但现在很多人失业,我就不用担心这种问题。”


这哥们曾经抱怨过无数次薪资低,甚至曾经咨询过我要跳出来应该做什么,彼时还帮他全面评估过,最终建议他继续呆着。


看着他这么开心的样子,我也为他开心,但又总觉得哪里不对劲。最后才想明白:如果一个人的开心要建立在这种基础上,那过两年经济好了,市场上的薪资高了,财务自由的机会多了,他是不是又要痛苦了?


当然我并不是想说:大家不要考公、不要进国企。而是想说:工作的选择除了从众也要考虑自己的爱好和特性,尤其要用以终为始的目标思维去思考:想一想20年后的自己,是一种什么职场状态,这种状态能否让你满意。


我们还要明白几乎不存在完美的工作,大部分工作都有两面性,不光要想好的一面,更重要是坏的一面能否接受。


这些问题,值得我们每一个人多思考思考。

来源:findyi ,作者findyi

收起阅读 »

两天两夜,1M图片优化到100kb!

坦白从宽吧,我就是那个花了两天两夜把 1M 图片优化到 100kb 的家伙——王小二! 自从因为一篇报道登上热搜后,我差点抑郁,每天要靠 50 片安眠药才能入睡。 网络上曝光的那些关于一码通的消息,有真有假,我这里就不再澄清了。就说说我是怎么把图片从 1M ...
继续阅读 »

坦白从宽吧,我就是那个花了两天两夜把 1M 图片优化到 100kb 的家伙——王小二!


自从因为一篇报道登上热搜后,我差点抑郁,每天要靠 50 片安眠药才能入睡。


网络上曝光的那些关于一码通的消息,有真有假,我这里就不再澄清了。就说说我是怎么把图片从 1M 优化到 100kb 的故事吧。


是的,由于系统群体规模和访问规模的特殊性,每一行代码、每一张图片、每一个技术文档都反复核准,优化再优化,精益求精。为确保系统运行得更高效,我们将一张图片从1MB压缩到500KB,再从500KB优化到100KB。


这样的工作在外人看起来,简单到就好像悄悄给学妹塞一张情书就能让她做我女朋友一样简单。


但殊不知,这其中蕴含着极高的技术含量!


不信,我给你们普及下。


一、图像压缩


图像压缩是数据压缩技术在数字图像上的应用,目的是减少图像数据中的冗余信息,从而用更加高效的格式存储和传输数据。


图像压缩可以是有损数据压缩,也可以是无损数据压缩。




怎么样?


是不是感觉图像压缩技术没有想象中那么简单了?


更多关于图像压缩的资料可参考以下链接。



机器之心:http://www.jiqizhixin.com/graph/techn…



二、Java数字图像处理


作为这次“20 多万外包项目”的“主力开发人员”,我这里就给大家介绍下 Java 数字图像处理技术吧,一开始我就是用它来处理图片的。


数字图像处理(Digital Image Processing)是通过计算机对图像进行去除噪声、增强、复原、分割、提取特征等处理的方法和技术。



输入的是图像信号,然后经过 DIP 进行有效的算法处理后,输出为数字信号。


为了压缩图像,我们需要读取图像并将其转换成 BufferedImage 对象,BufferedImage 是 Image 类的一个子类,描述了一个具有可访问的图像数据缓冲区,由 ColorModel 和 Raster 的图像数据组成。



废话我就不多说了,直接进入实战吧!


三、图像压缩实战


刚好我本地有一张之前用过的封面图,离 1M 只差 236 KB,可以拿来作为测试用。



这其中要用到 ImageIO 类,这是一个静态类,提供了一系列方法用来读和写图像,同时还可以对图像进行简单的编码和解码。


比如说通过 ImageIO.read() 可以将图像读取到 BufferedImage 对象:


File input = new File("ceshi.jpg");
BufferedImage image = ImageIO.read(input);

比如说通过 ImageIO.getImageWritersByFormatName() 可以返回一个Iterator,其中包含了通过命名格式对图像进行编码的 ImageWriter。


Iterator<ImageWriter> writers =  ImageIO.getImageWritersByFormatName("jpg");
ImageWriter writer = (ImageWriter) writers.next();

比如说通过 ImageIO.createImageOutputStream() 可以创建一个图像的输出流对象,有了该对象后就可以通过 ImageWriter.setOutput() 将其设置为输出流。


File compressedImageFile = new File("bbcompress.jpg");
OutputStream os =new FileOutputStream(compressedImageFile);
ImageOutputStream ios = ImageIO.createImageOutputStream(os);
writer.setOutput(ios);

紧接着,可以对 ImageWriter 进行一些参数配置,比如说压缩模式,压缩质量等等。


ImageWriteParam param = writer.getDefaultWriteParam();

param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionQuality(0.01f);

压缩模式一共有四种,MODE_EXPLICIT 是其中一种,表示 ImageWriter 可以根据后续的 set 的附加信息进行平铺和压缩,比如说接下来的 setCompressionQuality() 方法。


setCompressionQuality() 方法的参数是一个 0-1 之间的数,0.0 表示尽最大程度压缩,1.0 表示保证图像质量很重要。对于有损压缩方案,压缩质量应该控制文件大小和图像质量之间的权衡(例如,通过在写入 JPEG 图像时选择量化表)。 对于无损方案,压缩质量可用于控制文件大小和执行压缩所需的时间之间的权衡(例如,通过优化行过滤器并在写入 PNG 图像时设置 ZLIB 压缩级别)。


整体代码如下所示:


public class Demo {
public static void main(String[] args) {

try {
File input = new File("ceshi.jpg");
BufferedImage image = ImageIO.read(input);


Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
ImageWriter writer = (ImageWriter) writers.next();

File compressedImageFile = new File("bbcompress.jpg");
OutputStream os = new FileOutputStream(compressedImageFile);
ImageOutputStream ios = ImageIO.createImageOutputStream(os);
writer.setOutput(ios);


ImageWriteParam param = writer.getDefaultWriteParam();

param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionQuality(0.01f);

writer.write(null, new IIOImage(image, null, null), param);

os.close();
ios.close();
writer.dispose();

} catch (IOException e) {
e.printStackTrace();
}
}
}

执行压缩后,可以看到图片的大小压缩到了 19 KB:



可以看得出,质量因子为 0.01f 的时候图片已经有些失真了,可以适当提高质量因子比如说 0.5f,再来看一下。



图片质量明显提高了,但大小依然只有 64 KB,压缩效果还是值得信赖的。


四、其他开源库


接下来,推荐一些可以轻松集成到项目中的图像处理库吧,它们全都是免费的。


1)ImageJ,用 Java 编写的,可以编辑、分析、处理、保存和打印图像。



2)Apache Commons Imaging,一个读取和写入各种图像格式的库,包括快速解析图像信息(如大小,颜色,空间,ICC配置文件等)和元数据。



3)ImageMagick,可以读取和写入超过100种格式的图像,包括DPX、EXR、GIF、JPEG、JPEG-2000、PDF、PNG、Postscript、SVG和TIFF。还可以调整大小、翻转、镜像、旋转、扭曲、剪切和变换图像,调整图像颜色,应用各种特殊效果,包括绘制文本、线条、多边形、椭圆和贝塞尔曲线。



4)OpenCV,由BSD许可证发布,可以免费学习和商业使用,提供了包括 C/C++、Python 和 Java 等主流编程语言在内的接口。OpenCV 专为计算效率而设计,强调实时应用,可以充分发挥多核处理器的优势。



这里就以 OpenCV 为例,来演示一下图像压缩。当然了,OpenCV 用来压缩图像属于典型的大材小用。


第一步,添加 OpenCV 依赖到我们的项目当中,以 Maven 为例。


<dependency>
<groupId>org.openpnp</groupId>
<artifactId>opencv</artifactId>
<version>4.5.1-2</version>
</dependency>

第二步,要想使用 OpenCV,需要先初始化。


OpenCV.loadShared();

第三步,使用 OpenCV 读取图片。


Mat src = Imgcodecs.imread(imagePath);

第四步,使用 OpenCV 压缩图片。


MatOfInt dstImage = new MatOfInt(Imgcodecs.IMWRITE_JPEG_QUALITY, 1);
Imgcodecs.imwrite("resized_image.jpg", sourceImage, dstImage);

MatOfInt 的构造参数是一个可变参数,第一个参数 IMWRITE_JPEG_QUALITY 表示对图片的质量进行改变,第二个是质量因子,1-100,值越大表示质量越高。


执行代码后得到的图片如下所示:



借这个机会,来对比下 OpenCV 和 JDK 原生 API 在压缩图像时所使用的时间。


这是我本机的配置情况,早年买的顶配 iMac,也是我的主力机。一开始只有 16 G 内存,后来加了一个 16 G 内存条,不过最近半年电脑突然死机重启的频率明显提高了,不知道是不是 Big Sur 这个操作系统的问题还是电脑硬件老了。



结果如下所示:


opencvCompress压缩完成,所花时间:1070
jdkCompress压缩完成,所花时间:322

压缩后的图片大小差不多,都是 19 KB,并且质量因子都是最低值。



四、一点点心声


经过上面的技术分析后,相信你们都明白了,把1M图片优化到100kb实在是一件“不太容易”的事情。。。。


100KB 很小了吧?只有原来的 1/10。


要知道,我可是连续加班了两天两夜,不眠不休。



累到最后,我趴在电脑上都睡着了。


没想到哈喇子直接给电脑整短路了,我这才算是从梦里面吓醒来了!


😔,生活不易,且行且珍惜吧~


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

重学Android-EditText的进阶操作

EditText的进阶使用 EditText 是我们常用的输入框控件,平常我们只是使用它输入文本,这里记录一些它不太常见的操作和一些解决方案。 一、焦点的自动获取 如果一个页面内定义了EditText,那么有可能我们进入此页面的时候会自动弹起软键盘,(分机型,...
继续阅读 »

EditText的进阶使用


EditText 是我们常用的输入框控件,平常我们只是使用它输入文本,这里记录一些它不太常见的操作和一些解决方案。


一、焦点的自动获取


如果一个页面内定义了EditText,那么有可能我们进入此页面的时候会自动弹起软键盘,(分机型,有的会弹,有的不弹)。如果我们需要弹软键盘,我们制定给 EditText 设置


    android:focusable="true"
android:focusableInTouchMode="true"

但是如果我们不想这个页面进去就弹出软键盘,我们可以给根布局或者 EditText 的父布局设置 focusable 。


二、光标和背景的控制


默认的 EditText 是带下划线和粗光标的,我们可以对它们进行简单的修改


android:background="@null" //去掉了下划线

android:textCursorDrawable="@null" //去掉光标的颜色

自定义光标的颜色和宽度:


<shape xmlns:android="http://schemas.android.com/apk/res/android">

<size android:width="2dp" />

<solid android:color="#BDC7D8" />

</shape>

使用自定义光标


android:textCursorDrawable="@drawable/edittext_cursor"

三、限制小数点位数


我们可以通过监听 EditText 的文本变化的方式来改变文本值,我们还能通过 DigitsKeyListener 的方式监听文本的改变。


3.1 TextWatcher的方式

我们可以通过监听 EditText 的文本变化,比如我们只想要小数点后面2位数,我们就监听文本变化,点后面的2位数,如果多了就把他删除掉。


public class MoneyTextWatcher implements TextWatcher {
private EditText editText;
private int digits = 2;

public MoneyTextWatcher(EditText et) {
editText = et;
}
public MoneyTextWatcher setDigits(int d) {
digits = d;
return this;
}


@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {

}

@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
//删除“.”后面超过2位后的数据
if (s.toString().contains(".")) {
if (s.length() - 1 - s.toString().indexOf(".") > digits) {
s = s.toString().subSequence(0,
s.toString().indexOf(".") + digits+1);
editText.setText(s);
editText.setSelection(s.length()); //光标移到最后
}
}
//如果"."在起始位置,则起始位置自动补0
if (s.toString().trim().substring(0).equals(".")) {
s = "0" + s;
editText.setText(s);
editText.setSelection(2);
}

//如果起始位置为0,且第二位跟的不是".",则无法后续输入
if (s.toString().startsWith("0")
&& s.toString().trim().length() > 1) {
if (!s.toString().substring(1, 2).equals(".")) {
editText.setText(s.subSequence(0, 1));
editText.setSelection(1);
return;
}
}
}

@Override
public void afterTextChanged(Editable s) {

}
}


使用:


//默认两位小数
mEditText.addTextChangedListener(new MoneyTextWatcher(mEditText1));

//手动设置其他位数,例如3
mEditText.addTextChangedListener(new MoneyTextWatcher(mEditText1).setDigits(3);

3.2 DigitsKeyListener的方式

public class ETMoneyValueFilter extends DigitsKeyListener {

public ETMoneyValueFilter(int d) {
super(false, true);
digits = d;
}

private int digits = 2; //默认显示二位数的小数点

public ETMoneyValueFilter setDigits(int d) {
digits = d;
return this;
}

@Override
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
CharSequence out = super.filter(source, start, end, dest, dstart, dend);

if (out != null) {
source = out;
start = 0;
end = out.length();
}

int len = end - start;

if (len == 0) {
return source;
}

//以点开始的时候,自动在前面添加0
if (source.toString().equals(".") && dstart == 0) {
return "0.";
}
//如果起始位置为0,且第二位跟的不是".",则无法后续输入
if (!source.toString().equals(".") && dest.toString().equals("0")) {
return "";
}

int dlen = dest.length();

for (int i = 0; i < dstart; i++) {
if (dest.charAt(i) == '.') {
return (dlen - (i + 1) + len > digits) ?
"" :
new SpannableStringBuilder(source, start, end);
}
}

for (int i = start; i < end; ++i) {
if (source.charAt(i) == '.') {
if ((dlen - dend) + (end - (i + 1)) > digits)
return "";
else
break;
}
}

return new SpannableStringBuilder(source, start, end);
}
}

其实是和 TextWatcher 类似的方式,那么使用的时候我们这样使用:


//默认两位小数
mEditText.setFilters(new InputFilter[]{new MoneyValueFilter()});

//手动设置其他位数,例如3
mEditText.setFilters(new InputFilter[]{new MoneyValueFilter().setDigits(3)});

在Kotlin代码中是这样使用:


et_input.filters = arrayOf(ETMoneyValueFilter().setDigits(3))

这样就可以实现小数点后面二位数的控制,还顺便加入了.的判断,自动加0的操作。


四、EditText的Search操作


当此 EditText 软键盘弹起的时候,右下角的确定变为搜索,我们需要给 EditText 设置一个属性:


 android:imeOptions="actionSearch"

然后给软键盘设置一个监听


       //点击软键盘搜索按钮
etSearch.setOnKeyListener(new View.OnKeyListener() {

@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {

if (keyCode == KeyEvent.KEYCODE_ENTER) {
// 先隐藏键盘
((InputMethodManager) getSystemService(INPUT_METHOD_SERVICE))
.hideSoftInputFromWindow(TransactionHistorySearchActivity.this.getCurrentFocus()
.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);

if (isSearch){
isSearch = false;
if (!TextUtils.isEmpty(etSearch.getText().toString()))
searchHistory(etSearch.getText().toString());
}

}
return false;
}
});

这里使用一个flag来判断,是因为部分机型会回调2次。所以为了统一效果,我们使用拦截判断只调用一次。


当然Search的逻辑如果你使用 Kotlin + DataBinding 来实现,那么就更简单了。


        //执行搜索
fun doSearch() {
KeyboardUtils.hideSoftInput(mActivity)
scrollTopRefresh()
}

//搜索的删除
fun searchDel() {
mViewModel.mKeywordLiveData.value = ""
doSearch()
}

           <LinearLayout
android:layout_width="match_parent"
android:layout_height="32dp"
android:layout_marginLeft="@dimen/d_15dp"
android:background="@drawable/shape_search_gray_bg_corners20"
android:gravity="center_vertical"
android:orientation="horizontal">

<ImageView
android:layout_width="@dimen/d_16dp"
android:layout_height="@dimen/d_16dp"
android:layout_marginLeft="@dimen/d_12dp"
android:src="@drawable/search_icon"
binding:clicks="@{click.doSearch}" />

<EditText
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/d_12dp"
android:layout_weight="1"
android:background="@color/transparent"
android:hint="大家都在搜"
android:imeOptions="actionSearch"
android:singleLine="true"
android:text="@={viewModel.mKeywordLiveData}"
android:textColor="@color/black"
android:textColorHint="@color/gray_99"
android:textSize="@dimen/d_14sp"
binding:onKeyEnter="@{click.doSearch}"
binding:typefaceMedium="@{true}" />

<ImageView
android:layout_width="@dimen/d_16dp"
android:layout_height="@dimen/d_16dp"
android:layout_marginRight="@dimen/d_10dp"
android:src="@drawable/search_delete"
android:visibility="gone"
binding:clicks="@{click.searchDel}"
binding:isVisibleGone="@{!TextUtils.isEmpty(viewModel.MKeywordLiveData)}" />

</LinearLayout>

主要的 Binding Adapter 方法为 onKeyEnter ,它实现了软键盘的搜索。


下面是自定义BindingAdapter的方法:


var _viewClickFlag = false
var _clickRunnable = Runnable { _viewClickFlag = false }

/**
* Edit的确认按键事件
*/
@BindingAdapter("onKeyEnter")
fun EditText.onKeyEnter(action: () -> Unit) {
setOnKeyListener { _, keyCode, _ ->
if (keyCode == KeyEvent.KEYCODE_ENTER) {
KeyboardUtils.closeSoftKeyboard(this)

if (!_viewClickFlag) {
_viewClickFlag = true
action()
}
removeCallbacks(_clickRunnable)
postDelayed(_clickRunnable, 1000)
}
return@setOnKeyListener false
}
}

和上面Java的实现方式类似,同样的做了防抖的操作。为了部分机型连续调用多次的问题。


效果:



五、焦点与软键盘的自由控制


上面说到的焦点,不自动弹出软键盘,如果我想自由的控制焦点与软键盘怎么办?


一个例子来说明,比如我们的需求,点击 EditText 的时候弹出弹框提示用户注意事项,当点击确定或者取消之后再继续输入。分解步骤如下:



  1. 我们点击EditText不能弹出软键盘

  2. 监听焦点获取之后弹出弹框

  3. 弹框完成之后我们需要手动的给EditText焦点

  4. 获取焦点之后需要设置光标与软键盘


代码逻辑如下:


      mBankAccountEt.setShowSoftInputOnFocus(false);
mBankAccountEt.setOnFocusChangeListener((v, hasFocus) -> {
if (hasFocus && !isShowedBankAccountNotice) {
showBankAccountNoticePopup();
}
});


private void showBankAccountNoticePopup() {
BasePopupView mPopupView = new XPopup.Builder(mActivity)
.moveUpToKeyboard(false)
.hasShadowBg(true)
.asCustom(new BankNameNoticePopup(mActivity, () -> {
isShowedBankAccountNotice = true;
mBankAccountEt.setShowSoftInputOnFocus(true);

//需要把焦点设置回EditText
mBankAccountEt.setFocusable(true);
mBankAccountEt.setFocusableInTouchMode(true);
mBankAccountEt.requestFocus();
mBankAccountEt.setSelection(mBankAccountEt.getText().toString().length());

KeyboardUtils.showKeyboard(mBankAccountEt);
}));

if (mPopupView != null && !mPopupView.isShow()) {
mPopupView.show();
}
}

弹框就不给大家展示了,非常简单的弹窗,定义使用的弹窗库,逻辑都在完成的回调中。


KeyboardUtils工具类,控制EditText的软键盘展示与隐藏


	/*
* 显示键盘
* */
public static void showKeyboard(View view) {
InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
view.requestFocus();
imm.showSoftInput(view, 0);
}
}

/*
* 隐藏键盘
* */
public static void hideKeyboard(View view){
InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
imm.hideSoftInputFromWindow(view.getWindowToken(),0);
}
}

效果:



六、RV + EditText复用的问题


不知道大家有没有在RV中使用过 EditText ,Item中如果有 EditText 那么在滚出屏幕之后 再拉回来可能刚才输入的文本就消失了,或者换成不是刚才输入的文本了,是因为缓存复用,可能复用了别的Item上面的 EditText 控件。


有几种解决方法如下:


方法一: 强制的停用Recyclerview的复用


helper.setIsRecyclable(false);

但是RV就无法缓存与回收了,如果你的Item数量就是固定的并且不多,那么使用这个方法是最好的。


方法二: 通过监听焦点来添加或移除Edittext的TextChangedListener


@Override
protected void convert(BaseViewHolder helper, EdittextInRecyclerViewOfBean item) {
EditText editText = helper.getView(R.id.et);
editText.setText(item.getNum() + "");

TextWatcher textWatcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {

}

@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {

}

@Override
public void afterTextChanged(Editable s) {
//这里处理数据
if (TextUtils.isEmpty(s.toString())) {
item.setNum(0);
} else {
item.setNum(Integer.parseInt(s.toString()));
}
}
};

editText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (hasFocus){
editText.addTextChangedListener(textWatcher);
}else {
editText.removeTextChangedListener(textWatcher);
}
}
});
}


方法三: 通过view的setTag()方法解决


@Override
protected void convert(BaseViewHolder helper, EdittextInRecyclerViewOfBean item) {
TextWatcher textWatcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {

}

@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {

}

@Override
public void afterTextChanged(Editable s) {
if (TextUtils.isEmpty(s.toString())) {
item.setNum(0);
} else {
item.setNum(Integer.parseInt(s.toString()));
}
}
};

EditText editText = helper.getView(R.id.et);
//为了避免TextWatcher在调用settext()时被调用,提前将它移除
if (editText.getTag() instanceof TextWatcher) {
editText.removeTextChangedListener((TextWatcher) editText.getTag());
}
editText.setText(item.getNum() + "");
//重新添加上TextWatcher监听
editText.addTextChangedListener(textWatcher);
//将TextWatcher绑定到EditText
editText.setTag(textWatcher);
}


方法四: 为每个EditText的绑定位置


public class EditTextInRecyclerViewAdapter extends RecyclerView.Adapter {
private List<EdittextInRecyclerViewOfBean> mList = new ArrayList<>();

public void setData(List<EdittextInRecyclerViewOfBean> list) {
this.mList = list;
}

@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_edittext, parent, false);
return new ViewHolder(v, new ITextWatcher());
}

@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
ViewHolder viewHolder = (ViewHolder) holder;
viewHolder.mITextWatcher.bindPosition(position);
viewHolder.mEditText.setText(mList.get(position).getNum()+"");
}

@Override
public int getItemCount() {
return mList.size();
}

class ViewHolder extends RecyclerView.ViewHolder {
EditText mEditText;
ITextWatcher mITextWatcher;

private ViewHolder(View v, ITextWatcher watcher) {
super(v);
this.mEditText = v.findViewById(R.id.et);
this.mITextWatcher = watcher;
this.mEditText.addTextChangedListener(watcher);
}
}

class ITextWatcher implements TextWatcher {
private int position;

private void bindPosition(int position) {
this.position = position;
}

@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}

@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}

@Override
public void afterTextChanged(Editable s) {
if (TextUtils.isEmpty(s.toString())) {
mList.get(position).setNum(0);
} else {
mList.get(position).setNum(Integer.parseInt(s.toString()));
}
}
}
}


方法五: 构造方法中添加TextChanged


class PicViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

var ivPic: ImageView = itemView.findViewById(R.id.ivPic)
var etScore: EditText = itemView.findViewById(R.id.etScore)
var tvTitle: TextView = itemView.findViewById(R.id.tvTitle)
var myTextWatcher: MyTextWatcher = MyTextWatcher()

init {
etScore.addTextChangedListener(myTextWatcher)
}

fun updateView(picItem: PicItem) {
myTextWatcher.picItem = picItem
ivPic.setImageResource(picItem.picResId)
tvTitle.text = picItem.title
if (picItem.score == null) {
etScore.hint = "请输入分数"
etScore.setText("")
} else {
etScore.setText(picItem.score)
}
}
}

class MyTextWatcher: TextWatcher {

lateinit var picItem:PicItem

override fun afterTextChanged(s: Editable?) {
picItem?.apply {
score=s?.toString()
}
}

override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}

override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
}

方法六: 让产品改需求,不要使用EditText,或者我们干脆使用TextView,然后点击Item弹出输入框的弹框方式来实现。


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

Android架构之路--热更新Tinker

当前市面的热补丁方案有很多,其中比较出名的有阿里的 AndFix、美团的 Robust 以及 QZone 的超级补丁方案。但它们都存在无法解决的问题,这也是正是最后使用 Tinker 的原因。先看一张图对比:Tinker热补丁方案不仅支持类、So 以及资源的替...
继续阅读 »

一、简介

当前市面的热补丁方案有很多,其中比较出名的有阿里的 AndFix、美团的 Robust 以及 QZone 的超级补丁方案。但它们都存在无法解决的问题,这也是正是最后使用 Tinker 的原因。先看一张图对比:

1-1:热更新对比图

Tinker热补丁方案不仅支持类、So 以及资源的替换,它还是2.X-7.X的全平台支持。利用Tinker我们不仅可以用做 bugfix,甚至可以替代功能的发布。Tinker已运行在微信的数亿Android设备上。

TinkerPatch 平台在 Github 为大家提供了各种各样的 Sample,大家可点击前往 [TinkerPatch Github].

Tinker原理图

1-2:原理图

Tinker流程图

1-3:Tinker 流程图

二、Tinker相关网站

微信Tinker Patch官网:Tinker Patch
Github地址:tinker

三、接入Tinker步骤

基础步骤

  • 注册Tinker账户、添加APP、记录AppKey,添加 APP 版本、 发布补丁。详细步骤请移步Tinker平台使用文档

主要来说下配置Gradle和代码

1. 配置Tinker版本信息

我们使用配置文件去配置Tinker版本信息,易于统一版本和后面更换版本,如图:

2-1 gradle.properties文件

代码如下:

TINKER_VERSION=1.9.6
TINKERPATCH_VERSION=1.2.6

2. 使用Tinker插件

在根目录下的build.gradle文件下配置,如图:

2-2 添加Tinker插件



代码如下:

classpath "com.tinkerpatch.sdk:tinkerpatch-gradle-plugin:${TINKERPATCH_VERSION}"

3. 配置Tinker的gradle脚本

在项目app目录下新建tinkerparch.gradle文件,如图:

2-3 tinkerpatch.gradle

代码如下:

apply plugin: 'tinkerpatch-support'
/**
* TODO: 请按自己的需求修改为适应自己工程的参数
*/

def bakPath = file("${buildDir}/bakApk/")
def baseInfo = "app-1.0.0-0529-14-38-02"
def variantName = "release"
/**
* 对于插件各参数的详细解析请参考
* http://tinkerpatch.com/Docs/SDK
*/

tinkerpatchSupport {
/** 可以在debug的时候关闭 tinkerPatch **/
/** 当disable tinker的时候需要添加multiDexKeepProguard和proguardFiles,
* 这些配置文件本身由tinkerPatch的插件自动添加,当你disable后需要手动添加
* 你可以copy本示例中的proguardRules.pro和tinkerMultidexKeep.pro,
* 需要你手动修改'tinker.sample.android.app'本示例的包名为你自己的包名,
* com.xxx前缀的包名不用修改
**/

tinkerEnable = true
reflectApplication = true
/**
* 是否开启加固模式,只能在APK将要进行加固时使用,否则会patch失败。
* 如果只在某个渠道使用了加固,可使用多flavors配置
**/

protectedApp = false
/**
* 实验功能
* 补丁是否支持新增 Activity (新增Activity的exported属性必须为false)
**/

supportComponent = true

autoBackupApkPath = "${bakPath}"
/** 注意:换成自己在Tinker平台上申请的appKey**/
appKey = "521db2518e0ca16d"

/** 注意: 若发布新的全量包, appVersion一定要更新 **/
appVersion = "1.0.0"

def pathPrefix = "${bakPath}/${baseInfo}/${variantName}/"
def name = "${project.name}-${variantName}"

baseApkFile = "${pathPrefix}/${name}.apk"
baseProguardMappingFile = "${pathPrefix}/${name}-mapping.txt"
baseResourceRFile = "${pathPrefix}/${name}-R.txt"

/**
* 若有编译多flavors需求, 可以参照:
* https://github.com/TinkerPatch/tinkerpatch-flavors-sample
* 注意: 除非你不同的flavor代码是不一样的,
* 不然建议采用zip comment或者文件方式生成渠道信息
* (相关工具:walle 或者 packer-ng)
**/

}

/**
* 用于用户在代码中判断tinkerPatch是否被使用
*/

android {
defaultConfig {
buildConfigField "boolean", "TINKER_ENABLE",
"${tinkerpatchSupport.tinkerEnable}"
}
}

/**
* 一般来说,我们无需对下面的参数做任何的修改
* 对于各参数的详细介绍请参考:
* https://github.com/Tencent/tinker/wiki/Tinker-
* %E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97
*/

tinkerPatch {
ignoreWarning = false
useSign = true
dex {
dexMode = "jar"
pattern = ["classes*.dex"]
loader = []
}
lib {
pattern = ["lib/*/*.so"]
}

res {
pattern = ["res/*", "r/*", "assets/*", "resources.arsc",
"AndroidManifest.xml"]
ignoreChange = []
largeModSize = 100
}

packageConfig {
}
sevenZip {
zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
// path = "/usr/local/bin/7za"
}
buildConfig {
keepDexApply = false
}
}

注意

  • AppKey:换成自己在Tinker平台上申请的。
  • baseInfo:基准包名称,使用Tinker脚本编译在模块的build/bakApk生成编译副本。
  • variantName: 这个一般对应buildTypes里面你基准包生成的类型,release、debug或其他
  • appVersion:配置和Tinker后台新建补丁包的一致。

其他地方可以暂时不用改

4. 配置模块下的build.gradle

配置签名

如果有不会的同学可以看这篇 Android Studio的两种模式及签名配置

2-4:配置签名

在配置混淆代码的时候,想要提醒下大家,当设置 minifyEnabled 为false时代表不混淆代码,shrinkResources也应设置为false ,它们通常是彼此关联。
要是你设置minifyEnabled 为false,shrinkResources为true,将会报异常,信息如下:

Error:A problem was found with the configuration of task':watch:packageOfficialDebug'.
File '...\build\intermediates\res\resources-official-debug-stripped.ap_' specified for property 'resourceFile' does not exist.

2-4-1:混淆配置

配置依赖

2-5:配置Tinker依赖

使用插件

2-6:使用Tinker插件

具体代码如下:

apply plugin: 'com.android.application'
apply from: 'tinkerpatch.gradle'

android {
compileSdkVersion 25
buildToolsVersion "25.0.3"
defaultConfig {
applicationId "qqt.com.tinkerdemo"
minSdkVersion 17
targetSdkVersion 25
versionCode 1
versionName "1.0.0"
multiDexEnabled true
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
signingConfigs {
release {
storeFile file("./jks/tinker.jks")
storePassword "123456"
keyAlias "tinker"
keyPassword "123456"

}
debug {
storeFile file("./jks/debug.keystore")
}
}
buildTypes {
release {
minifyEnabled false
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
signingConfig signingConfigs.debug
}
}
}

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:25.3.1'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
testCompile 'junit:junit:4.12'

annotationProcessor("com.tinkerpatch.tinker:tinker-android-anno:${TINKER_VERSION}") {
changing = true
}
provided("com.tinkerpatch.tinker:tinker-android-anno:${TINKER_VERSION}") {
changing = true
}
compile("com.tinkerpatch.sdk:tinkerpatch-android-sdk:${TINKERPATCH_VERSION}") {
changing = true
}

compile 'com.android.support:multidex:1.0.1'
}

5. 代码集成

最后一步,在自己的代码新建一个Application,把代码集成在App中,别忘了在AndroidManifest里面配置APP。。。
如图:

2-7:集成代码



我是继承MultiDexApplication主要是防止64k异常。有关这块知识,请看 Android 方法数超过64k、编译OOM、编译过慢解决方案

具体代码如下:

package qqt.com.tinkerdemo;

import android.support.multidex.MultiDexApplication;

import com.tencent.tinker.loader.app.ApplicationLike;
import com.tinkerpatch.sdk.TinkerPatch;
import com.tinkerpatch.sdk.loader.TinkerPatchApplicationLike;

/**
* 邮箱:ljh12520jy@163.com
*
*
@author Ljh on 2018/5/28
*/


public class App extends MultiDexApplication {

private ApplicationLike mApplicationLike;

@Override
public void onCreate() {
super.onCreate();
mApplicationLike = TinkerPatchApplicationLike.getTinkerPatchApplicationLike();
// 初始化TinkerPatch SDK, 更多配置可参照API章节中的,初始化SDK
TinkerPatch.init(mApplicationLike)
.reflectPatchLibrary()
.fetchPatchUpdate(true)
// 强制更新
.setPatchRollbackOnScreenOff(true)
.setPatchRestartOnSrceenOff(true)
.setFetchPatchIntervalByHours(3);

// 每隔3个小时(通过setFetchPatchIntervalByHours设置)去访问后台时候有更新,
//通过handler实现轮训的效果
TinkerPatch.with().fetchPatchUpdateAndPollWithInterval();
}
}

四、生成基准包

在生成基准包的时候,要注意一个问题,就是关闭 instant run(当tinkerEnable = true时,false的时候,就不需要),如图:

3-1:关闭InstantRun

在Android Studio的右上角,点击Gradle,如图:

3-2:准备生成基准包

双击assembleRelease生成成功后安装模块/build/outputs/apk/release/app-release.apk就OK了,这时候进去模块/build/bakApk里面记录一下类似app-1.0.0-0530-18-01-59的文件名称,只生成一次基准包,那么就会生成一个。这里需要注意一下,如果点太多生成太多的话确定不了刚刚生成的是哪个,那么就选最新那个或者删掉重新生成基准包。
生成后的基准包如图:

3-3:生成基准包

五、修改bug

在自己的代码中随便修改点代码(Tinker1.9.6 里面支持新增Activity代码)

六、生成补丁包

在生成补丁包前,我们需要去tinkerpatch.gradle文件下修改一些信息。

  • baseInfo :把前面app-1.0.0-0529-14-38-02换成我们刚生成记录下的基准包(app-1.0.0-0530-18-01-59)就可以。
  • variantName : 因为刚刚我们使用assembleRelease生成的补丁,所以我们只需要使用release

双击TinkerPatchRelease生成差分包,patch_signed_7zip.apk就是补丁包

生成的补丁包如图:

3-4:生成补丁包

3-5:tinkerPatch下的一些文件说明

七、发布补丁包

回到Tinker后台,选中我们开始新建的项目,补丁下发->添加APP版本。然后上传刚刚的patch_signed_7zip.apk。

APP开启强制更新的话那么重启应用就会更新,否则会通过轮询去更新。应用重启才生效。

3-6:发布补丁包

注:在Tinker后台发布的差分包(补丁包)是根据app-1.0.0-0530-18-01-59为基准包下,修复bug生成的补丁包,只对于app-1.0.0-0530-18-01-59版本的apk生效。

3-7:差分包


一、多渠道打包

tinker官方文档推荐用walle或者packer-ng-plugin来辅助打渠道包。估计有不少同学用过,今天我想推荐另外一款多渠道打包的插件ApkMultiChannelPlugin,它作为Android Studio插件进行多渠道打包。
安装步骤:打开 Android Studio: 打开 Setting/Preferences -> Plugins -> Browse repositories 然后搜索 ApkMultiChannel 安装重启。
有不了解的同学,可以直接看它的文档。

我是采用add channel file to META-INF方式进行多渠道打包,在这里提供一个读取渠道的工具类ChannelHelper。

二、多渠道打包步骤

1. 选择一个基准包

选择基准包的一个apk,然后右键,点击Build MultiChannel

1-1:选择基准包

2. 配置

配置签名信息,打包方式和渠道等。

1-2:配置多渠道

配置说明:

Key Store Path: 签名文件的路径
Key Store Password: 签名文件的密码
Key Alias: 密钥别名
Key Password: 密钥密码

Zipalign Path: zipalign 文件的路径(用于优化 apk;zipalign 可以确保所有未压缩的数据均是以相对于文件开始部分的特定字节对齐开始,这样可减少应用消耗的 RAM 量。)
Signer Version: 选择签名版本:apksigner 和 jarsigner
Build Type: 打包方式

Channels: 渠道列表,每行一个,最前面可加 > 或不加(保存信息的时候,程序会自行加上)

我们刚才刚才配置的东西会保存在根目录的 channels.properties里

1-3:channel配置文件

3. 开始打包

配置完成后,选择基准包的一个apk,然后右键,点击Build MultiChannel,就会开始进行多渠道打包,文件会输出在选中的apk的当前目录下的channels是目录下,如图:

1-4:多渠道打包

4. 发布APK

将刚才打包完成的包,分别发布到对应的应用市场。

5. 修改bug

随便修改部分代码

6. 生成补丁包

在生成补丁包前,我们需要去tinkerpatch.gradle文件下修改一些信息。

  • baseInfo :改成我们刚才选择基准包的目录app-1.0.1-0601-14-30-42就可以。
    双击TinkerPatchRelease生成差分包,patch_signed_7zip.apk就是补丁包
  • variantName : 因为刚刚我们使用assembleRelease生成的补丁,所以我们只需要使用release

双击TinkerPatchRelease生成差分包,patch_signed_7zip.apk就是补丁包

1-5:生成补丁包

7. 发布补丁包

回到Tinker后台,选中我们开始新建的项目,补丁下发->添加APP版本。然后上传刚刚的patch_signed_7zip.apk。

APP开启强制更新的话那么重启应用就会更新,否则会通过轮询去更新。应用重启才生效。

1-6: 发布补丁包.png

注:

  1. 这个补丁包对于以app-1.0.1-0601-14-30-42为基准宝,进行多渠道打包的apk都能生效(亲测成功),如果你把该渠道包进行360加固(protectedApp = true),也生效。
  2. 当我们在正式环境需要混淆代码:设置 minifyEnabled true,添加混淆:
-keep public class * implements com.tencent.tinker.loader.app.ApplicationLike

如图:

1-7: 混淆代码

本文转载自: https://cloud.tencent.com/developer/article/1872556

收起阅读 »

AFNetworking源码探究 —— UIKit相关之UIProgressView+AFNetworking分类

iOS
下面我们先看一下接口的API/** This category adds methods to the UIKit framework's `UIProgressView` class. The methods in this category provide...
继续阅读 »

接口API

下面我们先看一下接口的API

/**
This category adds methods to the UIKit framework's `UIProgressView` class. The methods in this category provide support for binding the progress to the upload and download progress of a session task.
*/
@interface UIProgressView (AFNetworking)

///------------------------------------
/// @name Setting Session Task Progress
///------------------------------------

/**
Binds the progress to the upload progress of the specified session task.

@param task The session task.
@param animated `YES` if the change should be animated, `NO` if the change should happen immediately.
*/
- (void)setProgressWithUploadProgressOfTask:(NSURLSessionUploadTask *)task
animated:(BOOL)animated;

/**
Binds the progress to the download progress of the specified session task.

@param task The session task.
@param animated `YES` if the change should be animated, `NO` if the change should happen immediately.
*/
- (void)setProgressWithDownloadProgressOfTask:(NSURLSessionDownloadTask *)task
animated:(BOOL)animated;

@end


该类为UIKit框架的UIProgressView类添加方法。 此类别中的方法为将进度绑定到会话任务的上载和下载进度提供了支持。

该接口比较少,其实就是一个上传任务和一个下载任务分别和进度的绑定,可动画。

这里大家还要注意一个关于类的继承的细节。

// 上传
@interface NSURLSessionUploadTask : NSURLSessionDataTask
@interface NSURLSessionDataTask : NSURLSessionTask

// 下载
@interface NSURLSessionDownloadTask : NSURLSessionTask

给大家贴出来就是想让大家注意下这个结构。

runtime获取是否可动画

这里还是用runtime分别绑定下载和上传是否可动画。

- (BOOL)af_uploadProgressAnimated {
return [(NSNumber *)objc_getAssociatedObject(self, @selector(af_uploadProgressAnimated)) boolValue];
}

- (void)af_setUploadProgressAnimated:(BOOL)animated {
objc_setAssociatedObject(self, @selector(af_uploadProgressAnimated), @(animated), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (BOOL)af_downloadProgressAnimated {
return [(NSNumber *)objc_getAssociatedObject(self, @selector(af_downloadProgressAnimated)) boolValue];
}

- (void)af_setDownloadProgressAnimated:(BOOL)animated {
objc_setAssociatedObject(self, @selector(af_downloadProgressAnimated), @(animated), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

这个还算是很好理解的,有了前面的基础,这里就不多说了。

接口的实现

下面我们就看一下接口的实现。

1. 上传任务

static void * AFTaskCountOfBytesSentContext = &AFTaskCountOfBytesSentContext;
static void * AFTaskCountOfBytesReceivedContext = &AFTaskCountOfBytesReceivedContext;
- (void)setProgressWithUploadProgressOfTask:(NSURLSessionUploadTask *)task
animated:(BOOL)animated
{
if (task.state == NSURLSessionTaskStateCompleted) {
return;
}

[task addObserver:self forKeyPath:@"state" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesSentContext];
[task addObserver:self forKeyPath:@"countOfBytesSent" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesSentContext];

[self af_setUploadProgressAnimated:animated];
}

这里逻辑很清晰,简单的说一下,如果任务是完成状态,那么就直接return,然后给task添加KVO观察,观察属性是state和countOfBytesSent,最后就是设置是否可动画的状态。

2. 下载任务

- (void)setProgressWithDownloadProgressOfTask:(NSURLSessionDownloadTask *)task
animated:(BOOL)animated
{
if (task.state == NSURLSessionTaskStateCompleted) {
return;
}

[task addObserver:self forKeyPath:@"state" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesReceivedContext];
[task addObserver:self forKeyPath:@"countOfBytesReceived" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesReceivedContext];

[self af_setDownloadProgressAnimated:animated];
}

这里逻辑很清晰,简单的说一下,如果任务是完成状态,那么就直接return,然后给task添加KVO观察,观察属性是state和countOfBytesReceived,最后就是设置是否可动画的状态。


KVO观察实现

下面看一下KVO观察的实现,这里也是这个类的精髓所在。

- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(__unused NSDictionary *)change
context:(void *)context
{
if (context == AFTaskCountOfBytesSentContext || context == AFTaskCountOfBytesReceivedContext) {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesSent))]) {
if ([object countOfBytesExpectedToSend] > 0) {
dispatch_async(dispatch_get_main_queue(), ^{
[self setProgress:[object countOfBytesSent] / ([object countOfBytesExpectedToSend] * 1.0f) animated:self.af_uploadProgressAnimated];
});
}
}

if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesReceived))]) {
if ([object countOfBytesExpectedToReceive] > 0) {
dispatch_async(dispatch_get_main_queue(), ^{
[self setProgress:[object countOfBytesReceived] / ([object countOfBytesExpectedToReceive] * 1.0f) animated:self.af_downloadProgressAnimated];
});
}
}

if ([keyPath isEqualToString:NSStringFromSelector(@selector(state))]) {
if ([(NSURLSessionTask *)object state] == NSURLSessionTaskStateCompleted) {
@try {
[object removeObserver:self forKeyPath:NSStringFromSelector(@selector(state))];

if (context == AFTaskCountOfBytesSentContext) {
[object removeObserver:self forKeyPath:NSStringFromSelector(@selector(countOfBytesSent))];
}

if (context == AFTaskCountOfBytesReceivedContext) {
[object removeObserver:self forKeyPath:NSStringFromSelector(@selector(countOfBytesReceived))];
}
}
@catch (NSException * __unused exception) {}
}
}
}
}

这里还是很简单的吧。

  • 如果keyPath是@"countOfBytesSent",那么就获取countOfBytesExpectedToSend,计算进度百分比,在主线程调用[self setProgress:[object countOfBytesSent] / ([object countOfBytesExpectedToSend] * 1.0f) animated:self.af_uploadProgressAnimated];得到进度。
  • 如果keyPath是@"countOfBytesReceived",那么就获取countOfBytesExpectedToReceive,计算进度百分比,在主线程调用[self setProgress:[object countOfBytesReceived] / ([object countOfBytesExpectedToReceive] * 1.0f) animated:self. af_downloadProgressAnimated];得到进度。
  • 如果keyPath是@"state"并且任务是完成状态NSURLSessionTaskStateCompleted,那么就要移除对这几个keyPath的观察者。

后记

本篇主要分析了UIProgressView+AFNetworking分类,主要实现了上传任务和下载任务与进度之间的绑定。

转载自:https://cloud.tencent.com/developer/article/1872398





收起阅读 »

AFNetworking源码探究(二十五) —— UIKit相关之UIRefreshControl+AFNetworking分类

iOS
上一篇主要分析了UIProgressView+AFNetworking分类,主要实现了上传任务和下载任务与进度之间的绑定。这一篇主要分析UIRefreshControl+AFNetworking这个分类。接口API下面我们先看一下接口的API/** This ...
继续阅读 »

回顾

上一篇主要分析了UIProgressView+AFNetworking分类,主要实现了上传任务和下载任务与进度之间的绑定。这一篇主要分析UIRefreshControl+AFNetworking这个分类。


接口API

下面我们先看一下接口的API

/**
This category adds methods to the UIKit framework's `UIProgressView` class. The methods in this category provide support for binding the progress to the upload and download progress of a session task.
*/
@interface UIProgressView (AFNetworking)

///------------------------------------
/// @name Setting Session Task Progress
///------------------------------------

/**
Binds the progress to the upload progress of the specified session task.

@param task The session task.
@param animated `YES` if the change should be animated, `NO` if the change should happen immediately.
*/
- (void)setProgressWithUploadProgressOfTask:(NSURLSessionUploadTask *)task
animated:(BOOL)animated;

/**
Binds the progress to the download progress of the specified session task.

@param task The session task.
@param animated `YES` if the change should be animated, `NO` if the change should happen immediately.
*/
- (void)setProgressWithDownloadProgressOfTask:(NSURLSessionDownloadTask *)task
animated:(BOOL)animated;

@end

该类为UIKit框架的UIProgressView类添加方法。 此类别中的方法为将进度绑定到会话任务的上载和下载进度提供了支持。

该接口比较少,其实就是一个上传任务和一个下载任务分别和进度的绑定,可动画。

这里大家还要注意一个关于类的继承的细节。

// 上传
@interface NSURLSessionUploadTask : NSURLSessionDataTask
@interface NSURLSessionDataTask : NSURLSessionTask

// 下载
@interface NSURLSessionDownloadTask : NSURLSessionTask

给大家贴出来就是想让大家注意下这个结构。


runtime获取是否可动画

这里还是用runtime分别绑定下载和上传是否可动画。

- (BOOL)af_uploadProgressAnimated {
return [(NSNumber *)objc_getAssociatedObject(self, @selector(af_uploadProgressAnimated)) boolValue];
}

- (void)af_setUploadProgressAnimated:(BOOL)animated {
objc_setAssociatedObject(self, @selector(af_uploadProgressAnimated), @(animated), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (BOOL)af_downloadProgressAnimated {
return [(NSNumber *)objc_getAssociatedObject(self, @selector(af_downloadProgressAnimated)) boolValue];
}

- (void)af_setDownloadProgressAnimated:(BOOL)animated {
objc_setAssociatedObject(self, @selector(af_downloadProgressAnimated), @(animated), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

这个还算是很好理解的,有了前面的基础,这里就不多说了。


接口的实现

下面我们就看一下接口的实现。

1. 上传任务

static void * AFTaskCountOfBytesSentContext = &AFTaskCountOfBytesSentContext;
static void * AFTaskCountOfBytesReceivedContext = &AFTaskCountOfBytesReceivedContext;
- (void)setProgressWithUploadProgressOfTask:(NSURLSessionUploadTask *)task
animated:(BOOL)animated
{
if (task.state == NSURLSessionTaskStateCompleted) {
return;
}

[task addObserver:self forKeyPath:@"state" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesSentContext];
[task addObserver:self forKeyPath:@"countOfBytesSent" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesSentContext];

[self af_setUploadProgressAnimated:animated];
}

这里逻辑很清晰,简单的说一下,如果任务是完成状态,那么就直接return,然后给task添加KVO观察,观察属性是state和countOfBytesSent,最后就是设置是否可动画的状态。

2. 下载任务

- (void)setProgressWithDownloadProgressOfTask:(NSURLSessionDownloadTask *)task
animated:(BOOL)animated
{
if (task.state == NSURLSessionTaskStateCompleted) {
return;
}

[task addObserver:self forKeyPath:@"state" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesReceivedContext];
[task addObserver:self forKeyPath:@"countOfBytesReceived" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesReceivedContext];

[self af_setDownloadProgressAnimated:animated];
}

这里逻辑很清晰,简单的说一下,如果任务是完成状态,那么就直接return,然后给task添加KVO观察,观察属性是state和countOfBytesReceived,最后就是设置是否可动画的状态。


KVO观察实现

下面看一下KVO观察的实现,这里也是这个类的精髓所在。

- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(__unused NSDictionary *)change
context:(void *)context
{
if (context == AFTaskCountOfBytesSentContext || context == AFTaskCountOfBytesReceivedContext) {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesSent))]) {
if ([object countOfBytesExpectedToSend] > 0) {
dispatch_async(dispatch_get_main_queue(), ^{
[self setProgress:[object countOfBytesSent] / ([object countOfBytesExpectedToSend] * 1.0f) animated:self.af_uploadProgressAnimated];
});
}
}

if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesReceived))]) {
if ([object countOfBytesExpectedToReceive] > 0) {
dispatch_async(dispatch_get_main_queue(), ^{
[self setProgress:[object countOfBytesReceived] / ([object countOfBytesExpectedToReceive] * 1.0f) animated:self.af_downloadProgressAnimated];
});
}
}

if ([keyPath isEqualToString:NSStringFromSelector(@selector(state))]) {
if ([(NSURLSessionTask *)object state] == NSURLSessionTaskStateCompleted) {
@try {
[object removeObserver:self forKeyPath:NSStringFromSelector(@selector(state))];

if (context == AFTaskCountOfBytesSentContext) {
[object removeObserver:self forKeyPath:NSStringFromSelector(@selector(countOfBytesSent))];
}

if (context == AFTaskCountOfBytesReceivedContext) {
[object removeObserver:self forKeyPath:NSStringFromSelector(@selector(countOfBytesReceived))];
}
}
@catch (NSException * __unused exception) {}
}
}
}
}

这里还是很简单的吧。

  • 如果keyPath是@"countOfBytesSent",那么就获取countOfBytesExpectedToSend,计算进度百分比,在主线程调用[self setProgress:[object countOfBytesSent] / ([object countOfBytesExpectedToSend] * 1.0f) animated:self.af_uploadProgressAnimated];得到进度。
  • 如果keyPath是@"countOfBytesReceived",那么就获取countOfBytesExpectedToReceive,计算进度百分比,在主线程调用[self setProgress:[object countOfBytesReceived] / ([object countOfBytesExpectedToReceive] * 1.0f) animated:self. af_downloadProgressAnimated];得到进度。
  • 如果keyPath是@"state"并且任务是完成状态NSURLSessionTaskStateCompleted,那么就要移除对这几个keyPath的观察者。

后记

本篇主要分析了UIProgressView+AFNetworking分类,主要实现了上传任务和下载任务与进度之间的绑定。

本文转载自腾讯社区:作者conanma  https://cloud.tencent.com/developer/article/1872401



收起阅读 »

AFNetworking源码探究 —— UIKit相关之AFAutoPurgingImageCache缓存

iOS
回顾上一篇主要讲述了UIRefreshControl+AFNetworking这个分类,将刷新状态和任务状态进行了绑定和同步。这一篇主要讲述AFAutoPurgingImageCache有关的缓存。接口API按照老惯例,我们还是先看一下接口API文档。这个接口...
继续阅读 »

回顾

上一篇主要讲述了UIRefreshControl+AFNetworking这个分类,将刷新状态和任务状态进行了绑定和同步。这一篇主要讲述AFAutoPurgingImageCache有关的缓存。


接口API

按照老惯例,我们还是先看一下接口API文档。这个接口文档包括三个部分,两个协议一个类。

  • 协议AFImageCache
  • 协议AFImageRequestCache
  • AFAutoPurgingImageCache

1. AFImageCache

这个协议包括四个方法

/**
Adds the image to the cache with the given identifier.

@param image The image to cache.
@param identifier The unique identifier for the image in the cache.
*/
- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier;

/**
Removes the image from the cache matching the given identifier.

@param identifier The unique identifier for the image in the cache.

@return A BOOL indicating whether or not the image was removed from the cache.
*/
- (BOOL)removeImageWithIdentifier:(NSString *)identifier;

/**
Removes all images from the cache.

@return A BOOL indicating whether or not all images were removed from the cache.
*/
- (BOOL)removeAllImages;

/**
Returns the image in the cache associated with the given identifier.

@param identifier The unique identifier for the image in the cache.

@return An image for the matching identifier, or nil.
*/
- (nullable UIImage *)imageWithIdentifier:(NSString *)identifier;

该协议定义了包括加入、移除、获取缓存中的图片。

2. AFImageRequestCache

该协议包含下面几个方法,这里注意这个协议继承自协议AFImageCache

@protocol AFImageRequestCache <AFImageCache>

/**
Asks if the image should be cached using an identifier created from the request and additional identifier.

@param image The image to be cached.
@param request The unique URL request identifing the image asset.
@param identifier The additional identifier to apply to the URL request to identify the image.

@return A BOOL indicating whether or not the image should be added to the cache. YES will cache, NO will prevent caching.
*/
- (BOOL)shouldCacheImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;

/**
Adds the image to the cache using an identifier created from the request and additional identifier.

@param image The image to cache.
@param request The unique URL request identifing the image asset.
@param identifier The additional identifier to apply to the URL request to identify the image.
*/
- (void)addImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;

/**
Removes the image from the cache using an identifier created from the request and additional identifier.

@param request The unique URL request identifing the image asset.
@param identifier The additional identifier to apply to the URL request to identify the image.

@return A BOOL indicating whether or not all images were removed from the cache.
*/
- (BOOL)removeImageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;

/**
Returns the image from the cache associated with an identifier created from the request and additional identifier.

@param request The unique URL request identifing the image asset.
@param identifier The additional identifier to apply to the URL request to identify the image.

@return An image for the matching request and identifier, or nil.
*/
- (nullable UIImage *)imageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;

@end

根据请求和标识对图像进行是否需要缓存、加入到缓存或者移除缓存等进行操作。

3. AFAutoPurgingImageCache

这个是这个类的接口,大家注意下这个类遵循协议AFImageRequestCache

/**
The `AutoPurgingImageCache` in an in-memory image cache used to store images up to a given memory capacity. When the memory capacity is reached, the image cache is sorted by last access date, then the oldest image is continuously purged until the preferred memory usage after purge is met. Each time an image is accessed through the cache, the internal access date of the image is updated.
*/
@interface AFAutoPurgingImageCache : NSObject <AFImageRequestCache>

/**
The total memory capacity of the cache in bytes.
*/
// 内存缓存总的字节数
@property (nonatomic, assign) UInt64 memoryCapacity;

/**
The preferred memory usage after purge in bytes. During a purge, images will be purged until the memory capacity drops below this limit.
*/
// 以字节为单位清除后的首选内存使用情况。 在清除过程中,图像将被清除,直到内存容量降至此限制以下。
@property (nonatomic, assign) UInt64 preferredMemoryUsageAfterPurge;

/**
The current total memory usage in bytes of all images stored within the cache.
*/
// 当前所有图像内存缓存使用的总的字节数
@property (nonatomic, assign, readonly) UInt64 memoryUsage;

/**
Initialies the `AutoPurgingImageCache` instance with default values for memory capacity and preferred memory usage after purge limit. `memoryCapcity` defaults to `100 MB`. `preferredMemoryUsageAfterPurge` defaults to `60 MB`.
// 初始化,memoryCapcity为100M,preferredMemoryUsageAfterPurge为60M

@return The new `AutoPurgingImageCache` instance.
*/
- (instancetype)init;

/**
Initialies the `AutoPurgingImageCache` instance with the given memory capacity and preferred memory usage
after purge limit.

@param memoryCapacity The total memory capacity of the cache in bytes.
@param preferredMemoryCapacity The preferred memory usage after purge in bytes.

@return The new `AutoPurgingImageCache` instance.
*/
- (instancetype)initWithMemoryCapacity:(UInt64)memoryCapacity preferredMemoryCapacity:(UInt64)preferredMemoryCapacity;

@end

内存中图像缓存中的AutoPurgingImageCache用于存储图像到给定内存容量。 达到内存容量时,图像缓存按上次访问日期排序,然后最旧的图像不断清除,直到满足清除后的首选内存使用量。 每次通过缓存访问图像时,图像的内部访问日期都会更新。


AFAutoPurgingImageCache接口及初始化

从接口描述中我们可以看出来,类的初始化规定了内存总的使用量以及清楚以后的内存最优大小。

- (instancetype)init {
return [self initWithMemoryCapacity:100 * 1024 * 1024 preferredMemoryCapacity:60 * 1024 * 1024];
}

- (instancetype)initWithMemoryCapacity:(UInt64)memoryCapacity preferredMemoryCapacity:(UInt64)preferredMemoryCapacity {
if (self = [super init]) {
self.memoryCapacity = memoryCapacity;
self.preferredMemoryUsageAfterPurge = preferredMemoryCapacity;
self.cachedImages = [[NSMutableDictionary alloc] init];

NSString *queueName = [NSString stringWithFormat:@"com.alamofire.autopurgingimagecache-%@", [[NSUUID UUID] UUIDString]];
self.synchronizationQueue = dispatch_queue_create([queueName cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_CONCURRENT);

[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(removeAllImages)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];

}
return self;
}

我们看一下这个初始化方法中都做了什么事情

设置置缓存图像的字典

self.cachedImages = [[NSMutableDictionary alloc] init];

常见和UUID关联的并发队列

NSString *queueName = [NSString stringWithFormat:@"com.alamofire.autopurgingimagecache-%@", [[NSUUID UUID] UUIDString]];
self.synchronizationQueue = dispatch_queue_create([queueName cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_CONCURRENT);

增加移除所有图像的通知

[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(removeAllImages)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
- (BOOL)removeAllImages {
__block BOOL removed = NO;
dispatch_barrier_sync(self.synchronizationQueue, ^{
if (self.cachedImages.count > 0) {
[self.cachedImages removeAllObjects];
self.currentMemoryUsage = 0;
removed = YES;
}
});
return removed;
}

这里就是在上面生成的队列中,清空数组,重置一些属性值。


AFCachedImage接口及初始化

这里我们就看一下AFCachedImage的接口及初始化。

@interface AFCachedImage : NSObject

@property (nonatomic, strong) UIImage *image;
@property (nonatomic, strong) NSString *identifier;
@property (nonatomic, assign) UInt64 totalBytes;
@property (nonatomic, strong) NSDate *lastAccessDate;
@property (nonatomic, assign) UInt64 currentMemoryUsage;

@end

- (instancetype)initWithImage:(UIImage *)image identifier:(NSString *)identifier {
if (self = [self init]) {
self.image = image;
self.identifier = identifier;

CGSize imageSize = CGSizeMake(image.size.width * image.scale, image.size.height * image.scale);
CGFloat bytesPerPixel = 4.0;
CGFloat bytesPerSize = imageSize.width * imageSize.height;
self.totalBytes = (UInt64)bytesPerPixel * (UInt64)bytesPerSize;
self.lastAccessDate = [NSDate date];
}
return self;
}

这个初始化方法里面初始化图像的字节数,并更新上次获取数据的时间。


协议方法的实现

1. AFImageCache协议的实现

将图像根据标识添加到内存

- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier;
- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier {
dispatch_barrier_async(self.synchronizationQueue, ^{
AFCachedImage *cacheImage = [[AFCachedImage alloc] initWithImage:image identifier:identifier];

AFCachedImage *previousCachedImage = self.cachedImages[identifier];
if (previousCachedImage != nil) {
self.currentMemoryUsage -= previousCachedImage.totalBytes;
}

self.cachedImages[identifier] = cacheImage;
self.currentMemoryUsage += cacheImage.totalBytes;
});

dispatch_barrier_async(self.synchronizationQueue, ^{
if (self.currentMemoryUsage > self.memoryCapacity) {
UInt64 bytesToPurge = self.currentMemoryUsage - self.preferredMemoryUsageAfterPurge;
NSMutableArray <AFCachedImage*> *sortedImages = [NSMutableArray arrayWithArray:self.cachedImages.allValues];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastAccessDate"
ascending:YES];
[sortedImages sortUsingDescriptors:@[sortDescriptor]];

UInt64 bytesPurged = 0;

for (AFCachedImage *cachedImage in sortedImages) {
[self.cachedImages removeObjectForKey:cachedImage.identifier];
bytesPurged += cachedImage.totalBytes;
if (bytesPurged >= bytesToPurge) {
break ;
}
}
self.currentMemoryUsage -= bytesPurged;
}
});
}

这里用到了两个阻塞

第一个阻塞

dispatch_barrier_async(self.synchronizationQueue, ^{
AFCachedImage *cacheImage = [[AFCachedImage alloc] initWithImage:image identifier:identifier];

AFCachedImage *previousCachedImage = self.cachedImages[identifier];
if (previousCachedImage != nil) {
self.currentMemoryUsage -= previousCachedImage.totalBytes;
}

self.cachedImages[identifier] = cacheImage;
self.currentMemoryUsage += cacheImage.totalBytes;
});

这里的作用其实很清楚,就是先根据image和identify实例化AFCachedImage对象。然后在字典中根据identifier查看是否有AFCachedImage对象,如果有的话,那么就减小当前使用内存的值。并将前面实例化的AFCachedImage对象存入字典中,并增加当前使用内存的值。

第二个阻塞

dispatch_barrier_async(self.synchronizationQueue, ^{
if (self.currentMemoryUsage > self.memoryCapacity) {
UInt64 bytesToPurge = self.currentMemoryUsage - self.preferredMemoryUsageAfterPurge;
NSMutableArray <AFCachedImage*> *sortedImages = [NSMutableArray arrayWithArray:self.cachedImages.allValues];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastAccessDate"
ascending:YES];
[sortedImages sortUsingDescriptors:@[sortDescriptor]];

UInt64 bytesPurged = 0;

for (AFCachedImage *cachedImage in sortedImages) {
[self.cachedImages removeObjectForKey:cachedImage.identifier];
bytesPurged += cachedImage.totalBytes;
if (bytesPurged >= bytesToPurge) {
break ;
}
}
self.currentMemoryUsage -= bytesPurged;
}
});

这里完成的功能是,首先判断如果当前内存使用量大于内存总量,那么就需要清理了,这里需要计算需要清理多少内存,就是当前内存值 - 最优内存值。然后sortedImages实例化字典中所有的图片,并对这些图片进行按照时间的排序,遍历这个排序后的数组,逐一从字典中移除,终止条件就是移除的字节数大于上面计算的要清除的字节数值。最后,更新下当前内存使用的值。

根据指定标识将图像移出内存

- (BOOL)removeImageWithIdentifier:(NSString *)identifier;
- (BOOL)removeImageWithIdentifier:(NSString *)identifier {
__block BOOL removed = NO;
dispatch_barrier_sync(self.synchronizationQueue, ^{
AFCachedImage *cachedImage = self.cachedImages[identifier];
if (cachedImage != nil) {
[self.cachedImages removeObjectForKey:identifier];
self.currentMemoryUsage -= cachedImage.totalBytes;
removed = YES;
}
});
return removed;
}

这个还是很好理解的,在定义的并行队列中,取出identifier对应的AFCachedImage对象,然后从字典中移除,并更新当前内存的值。

从内存中移除所有的图像

- (BOOL)removeAllImages;
- (BOOL)removeAllImages {
__block BOOL removed = NO;
dispatch_barrier_sync(self.synchronizationQueue, ^{
if (self.cachedImages.count > 0) {
[self.cachedImages removeAllObjects];
self.currentMemoryUsage = 0;
removed = YES;
}
});
return removed;
}

其实就是一句话,清空字典,更新当前内存使用值。

根据指定的标识符从内存中获取图像

- (nullable UIImage *)imageWithIdentifier:(NSString *)identifier;
- (nullable UIImage *)imageWithIdentifier:(NSString *)identifier {
__block UIImage *image = nil;
dispatch_sync(self.synchronizationQueue, ^{
AFCachedImage *cachedImage = self.cachedImages[identifier];
image = [cachedImage accessImage];
});
return image;
}
- (UIImage*)accessImage {
self.lastAccessDate = [NSDate date];
return self.image;
}

其实就是从字典中取值,并更新上次获取图像的时间。

2. AFImageRequestCache协议的实现

根据请求和标识符将图像加入到内存

- (void)addImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;
- (void)addImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)identifier {
[self addImage:image withIdentifier:[self imageCacheKeyFromURLRequest:request withAdditionalIdentifier:identifier]];
}
- (NSString *)imageCacheKeyFromURLRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)additionalIdentifier {
NSString *key = request.URL.absoluteString;
if (additionalIdentifier != nil) {
key = [key stringByAppendingString:additionalIdentifier];
}
return key;
}

这里其实是调用上面我们讲过的那个根据identifier取出AFCachedImage对象的那个方法。不过下面这个identifier是通过调用下面这个方法生成的。、

根据请求和标识符将图像移出内存

- (BOOL)removeImageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;
- (BOOL)removeImageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)identifier {
return [self removeImageWithIdentifier:[self imageCacheKeyFromURLRequest:request withAdditionalIdentifier:identifier]];
}

这个,就是还是利用那个生成indentifier的方法,获取identify,然后调用前面我们讲过的方法移除对应的图像。

根据请求和标识符获取内存中图像

- (nullable UIImage *)imageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;
- (nullable UIImage *)imageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)identifier {
return [self imageWithIdentifier:[self imageCacheKeyFromURLRequest:request withAdditionalIdentifier:identifier]];
}

这个,就是还是利用那个生成indentifier的方法,获取identify,然后调用前面我们讲过的方法获取对应的图像。

是否将图像缓存到内存

- (BOOL)shouldCacheImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;
- (BOOL)shouldCacheImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier {
return YES;
}

这里就是写死的,默认就是需要进行缓存。

后记

本篇主要讲述了关于图像缓存方面的内容,包括使用标识符或者请求进行图像相关的缓存操作

本文转载自:https://cloud.tencent.com/developer/article/1872403

收起阅读 »

Idea不推荐使用@Autowired进行Field注入的原因

大家在使用IDEA开发的时候有没有注意到过一个提示,在字段上使用Spring的依赖注入注解@Autowired后会出现如下警告 Field injection is not recommended (字段注入是不被推荐的) 但是使用@Resource却不会...
继续阅读 »

大家在使用IDEA开发的时候有没有注意到过一个提示,在字段上使用Spring的依赖注入注解@Autowired后会出现如下警告



Field injection is not recommended (字段注入是不被推荐的)



但是使用@Resource却不会出现此提示


网上文章大部分都是介绍两者的区别,没有提到为什么,当时想了好久想出了可能的原因,今天来总结一下


Spring常见的DI方式



  • 构造器注入:利用构造方法的参数注入依赖

  • Setter注入:调用Setter的方法注入依赖

  • 字段注入:在字段上使用@Autowired/Resource注解


@Autowired VS @Resource


事实上,他们的基本功能都是通过注解实现依赖注入,只不过@AutowiredSpring定义的,而@ResourceJSR-250定义的。大致功能基本相同,但是还有一些细节不同:



  • 依赖识别方式:@Autowired默认是byType可以使用@Qualifier指定Name,@Resource默认ByName如果找不到则ByType

  • 适用对象:@Autowired可以对构造器、方法、参数、字段使用,@Resource只能对方法、字段使用

  • 提供方:@Autowired是Spring提供的,@Resource是JSR-250提供的


各种DI方式的优缺点


参考Spring官方文档,建议了如下的使用场景:



  • 构造器注入强依赖性(即必须使用此依赖),不变性(各依赖不会经常变动)

  • Setter注入可选(没有此依赖也可以工作),可变(依赖会经常变动)

  • Field注入:大多数情况下尽量少使用字段注入,一定要使用的话, @Resource相对@Autowired对IoC容器的耦合更低


Field注入的缺点



  • 不能像构造器那样注入不可变的对象

  • 依赖对外部不可见,外界可以看到构造器和setter,但无法看到私有字段,自然无法了解所需依赖

  • 会导致组件与IoC容器紧耦合(这是最重要的原因,离开了IoC容器去使用组件,在注入依赖时就会十分困难)

  • 导致单元测试也必须使用IoC容器,原因同上

  • 依赖过多时不够明显,比如我需要10个依赖,用构造器注入就会显得庞大,这时候应该考虑一下此组件是不是违反了单一职责原则


为什么IDEA只对@Autowired警告


Field注入虽然有很多缺点,但它的好处也不可忽略:那就是太方便了。使用构造器或者setter注入需要写更多业务无关的代码,十分麻烦,而字段注入大幅简化了它们。并且绝大多数情况下业务代码和框架就是强绑定的,完全松耦合只是一件理想上的事,牺牲了敏捷度去过度追求松耦合反而得不偿失。


那么问题来了,为什么IDEA只对@Autowired警告,却对@Resource视而不见呢?


个人认为,就像我们前面提到过的: @AutowiredSpring提供的,它是特定IoC提供的特定注解,这就导致了应用与框架的强绑定,一旦换用了其他的IoC框架,是不能够支持注入的。而 @ResourceJSR-250提供的,它是Java标准,我们使用的IoC容器应当去兼容它,这样即使更换容器,也可以正常工作。


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

官方core-ktx库能对富文本Span开发带来哪些便利?

当前SpannableStringBuilder的使用现状private fun test() { val stringBuilder = SpannableStringBuilder() var length = stringBuilder....
继续阅读 »

当前SpannableStringBuilder的使用现状

private fun test() {
val stringBuilder = SpannableStringBuilder()
var length = stringBuilder.length
stringBuilder.append("开始了")
//设置文本大小
stringBuilder.setSpan(
RelativeSizeSpan(20f),
length,
stringBuilder.length,
Spannable.SPAN_INCLUSIVE_EXCLUSIVE
)
length = stringBuilder.length
stringBuilder.append("执行了")
//设置背景颜色
stringBuilder.setSpan(
BackgroundColorSpan(Color.parseColor("#ffffff")),
length,
stringBuilder.length,
Spannable.SPAN_INCLUSIVE_EXCLUSIVE
)
length = stringBuilder.length
stringBuilder.append("结束了")
//设置点击事件
stringBuilder.setSpan(
object : ClickableSpan() {
override fun onClick(widget: View) {
}
},
length,
stringBuilder.length,
Spannable.SPAN_INCLUSIVE_EXCLUSIVE
)
}

以上代码就实现了三个功能,设置文本大小、背景颜色及点击事件,却写了这么一大坨代码,写起来好麻烦!!

core-ktx库的SpannableStringBuilder扩展

  1. 看下如何构造一个SpannableStringBuilder:

    image.png

    我们就可以在代码中这样使用:

    private fun test4() {
    val build = buildSpannedString {
    //操作各种Span
    }
    }

    请注意,这个buildSpannedString()方法的函数类型属于带接收者的函数类型,意为着我们可以访问SpannableStringBuilder定义的公共的属性方法(包括扩展方法),接下来我们就看下core-ktx库为SpannableStringBuilder提供了哪些扩展方法。

  2. SpannableStringBuilder.backgroundColor()设置背景色:

    image.png

    这个扩展方法需要传入一个颜色值充当背景色,backgroundColor()会自动帮助我们创建一个ForegroundColorSpan对象;还可以传入一个函数类型builderAction,比如用作使用append()方法设置要渲染的文本内容,最终会调用到inSpan()方法:

    image.png

    是不是明白了,最终我们是在这个方法中将xxxSpan设置给SpannableStringBuilder的。最终就可以这样使用了:

    val build = buildSpannedString {
    //操作各种Span
    backgroundColor(Color.RED) {
    append("开始了")
    }
    }
  3. SpannableStringBuilder.bold()设置粗体:

    image.png

    可以看到bold()方法中会自动帮助我们创建一个StyleSpan对象,使用起来和上面差不多:

    val build = buildSpannedString {
    bold {
    append("开始了")
    }
    }
  4. 其他SpannableStringBuilder.xxx()富文本设置扩展:

    core-ktx库提供了很多富文本设置的扩展方法,这里就只介绍上面的两个,其他的就不再这里介绍了,可以自行看下源码:

    image.png

  5. 一个非常非常简单的使用技巧

    假设当前有一小段文本遮天是一群人的完美,完美是一个人的遮天,我想要对整段文本设置一个背景色,对一群人这三个字设置一个粗体大小,利用上面core-ktx库提供的扩展,我们可以这样实现:

    private fun test4() {
    val build = buildSpannedString {
    backgroundColor(Color.RED) {
    append("遮天是")
    bold {
    append("一群人")
    }
    append("的完美,完美是一个人的遮天")
    }
    }
    }

    核心就是SpannableStringBuilder.xxx()系列的富文本扩展方法的第二个参数是一个接收者为SpannableStringBuilder的函数类型,所以backgroundColor()bold()strikeThrough()等等可以相互嵌套使用,从来更简单的实现一些富文本效果。

使用时请注意,buildSpannedString()这个方法创建的SpannableStringBuilder最终会包装成一个SpannedString不可变对象,请根据实际情况使用。

core-ktx库的Spannable扩展

SpannableStringBuilderSpannableString等实现了Spannable接口,所以Spannable定义的扩展方法对常用的SpannableStringBuilderSpannableString同样适用。

  1. Spannable.clearSpans清理所有标识(包括各种Span)

    image.png

    使用时,直接对Spannable及其子类调用clearSpans()即可。

  2. Spannable.set(start: Int, end: Int, span: Any)设置Span

    image.png

    这个扩展方法就比较牛逼了,它是一个运算符重载函数且重载了[xxx]运算符来设置Span的,我们看下使用:

    val stringBuilder = SpannableStringBuilder()
    //设置背景色
    stringBuilder[0, 2] = BackgroundColorSpan(Color.RED)

    有没有眼前一亮的感觉哈!!

  3. Spannable.set(range: IntRange, span: Any)设置Span

    image.png

    这个方法和上一个方法很像,不过传入的设置Span标识范围的方式发生了改变,变成了一个IntRange类型,我们直接看下使用:

    val stringBuilder = SpannableStringBuilder()
    //设置背景色
    stringBuilder[0..3] = BackgroundColorSpan(Color.RED)
  4. CharSequence.toSpannable()转换CharSequenceSpannableString

    image.png

    这个很简单,就不再进行举例说明了。

core-ktx库的Spanned扩展

Spanned的子接口包括我们上面刚讲到的Spannable,所以它定义的扩展方法对于SpannableStringBuilderSpannableString同样适用。

  1. CharSequence.toSpanned()转换CharSequenceSpannedString

    image.png

    注意和isSpannable()转换的区别,一个能设置Span,一个不能设。

  2. Spanned.getSpans()获取指定类型的Span标识

    image.png

    借助于Kotlin的泛型实化reified+inline简化了传入具体Span类型的逻辑,我们看下使用:

    private fun test4(builder: SpannableStringBuilder) {
    val spans = builder.getSpans<BackgroundColorSpan>()
    }

    获取类型为BackgroudColorSpan的所有Span对象,如果我们想要获取所有的Span对象,直接将传入的泛型类型改为Any即可。

  3. Spanned.toHtml()将富文本转换成同等效果显示的html代码

    image.png

    也就是说如果你富文本中存在ImageSpan,转换成html代码时,就会帮你在对应位置添加一个<img src="" />的标签,我们简单看下其核心源码Html.withinParagraph()中的片段:

    image.png

富文本绘制复杂布局的两种技巧

  1. ReplacementSpan这个Span使用非常灵活,它提供了方法draw()可自定义绘制你想要的布局效果;

  2. 如果使用ReplacementSpan自定义绘制布局还是太过于复杂,可以考虑先使用原生组件在xml中实现这个布局效果,然后将这个xml通过Inflate转换成View,并将调用ViewonDraw()方法,手动绘制到我们自定义Bitmap中,经过这个流程,我们就将这个复杂的布局转换成了Bitmap图像,然后使用ImageSpan加载该Bitmap,最终渲染到富文本中即可。

    请注意,请根据实际情况判断,是否需要先手动测量这个转换的View,然后再将其绘制到我们自定义的Bitmap中,否则可能不生效。

总结

以上就是core-ktx库针对于富文本提供的所有扩展方法,核心的源码就在SpannableStringBuilder.ktSpannableString.ktSpannedString.kt这三个文件中,大家有需要请自行查看。


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

收起阅读 »

Android 无障碍监听通知的过程

监听通知 Android 中的 AccessibilityService 可以监听通知信息的变化,首先需要创建一个无障碍服务,这个教程可以自行百度。在无障碍服务的配置文件中,需要以下配置: <accessibility-service ... and...
继续阅读 »

监听通知


Android 中的 AccessibilityService 可以监听通知信息的变化,首先需要创建一个无障碍服务,这个教程可以自行百度。在无障碍服务的配置文件中,需要以下配置:


<accessibility-service
...
android:accessibilityEventTypes="其他内容|typeNotificationStateChanged"
android:canRetrieveWindowContent="true" />

然后在 AccessibilityService 的 onAccessibilityEvent 方法中监听消息:


override fun onAccessibilityEvent(event: AccessibilityEvent?) {
when (event.eventType) {
AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED -> {
Log.d(Tag, "Notification: $event")
}
}
}

当有新的通知或 Toast 出现时,在这个方法中就会收到 AccessibilityEvent 。


另一种方案是通过 NotificationListenerService 进行监听,这里不做详细介绍了。两种方案的应用场景不同,推荐使用 NotificationListenerService 而不是无障碍服务。stackoverflow 上一个比较好的回答:



It depends on WHY you want to read it. The general answer would be Notification Listener. Accessibility Services are for unique accessibility services. A user has to enable an accessibility service from within the Accessibility Service menu (where TalkBack and Switch Access are). Their ability to read notifications is a secondary ability, to help them achieve the goal of creating assistive technologies (alternative ways for people to interact with mobile devices).


Whereas, Notification Listeners, this is their primary goal. They exist as part of the context of an app and as such don't need to be specifically turned on from the accessibility menu.


Basically, unless you are in fact building an accessibility service, you should not use this approach, and go with the generic Notification Listener.



无障碍服务监听通知逻辑


从用法中可以看出一个关键信息 -- TYPE_NOTIFICATION_STATE_CHANGED ,通过这个事件类型入手,发现它用于两个类中:



  • ToastPresenter:用于在应用程序进程中展示系统 UI 样式的 Toast 。

  • NotificationManagerService:通知管理服务。


ToastPresenter


ToastPresenter 的 trySendAccessibilityEvent 方法中,构建了一个 TYPE_NOTIFICATION_STATE_CHANGED 类型的消息:


public void trySendAccessibilityEvent(View view, String packageName) {
if (!mAccessibilityManager.isEnabled()) {
return;
}
AccessibilityEvent event = AccessibilityEvent.obtain(
AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
event.setClassName(Toast.class.getName());
event.setPackageName(packageName);
view.dispatchPopulateAccessibilityEvent(event);
mAccessibilityManager.sendAccessibilityEvent(event);
}

这个方法的调用在 ToastPresenter 中的 show 方法中:


public void show(View view, IBinder token, IBinder windowToken, int duration, int gravity,
int xOffset, int yOffset, float horizontalMargin, float verticalMargin,
@Nullable ITransientNotificationCallback callback) {
// ...
trySendAccessibilityEvent(mView, mPackageName);
// ...
}

而这个方法的调用就是在 Toast 中的 TN 类中的 handleShow 方法。


Toast.makeText(this, "", Toast.LENGTH_SHORT).show()

在 Toast 的 show 方法中,获取了一个 INotificationManager ,这个是 NotificationManagerService 在客户端暴露的 Binder 对象,通过这个 Binder 对象的方法可以调用 NMS 中的逻辑。


也就是说,Toast 的 show 方法调用了 NMS :


public void show() {
// ...
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
final int displayId = mContext.getDisplayId();

try {
if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
if (mNextView != null) {
// It's a custom toast
service.enqueueToast(pkg, mToken, tn, mDuration, displayId);
} else {
// It's a text toast
ITransientNotificationCallback callback = new CallbackBinder(mCallbacks, mHandler);
service.enqueueTextToast(pkg, mToken, mText, mDuration, displayId, callback);
}
} else {
service.enqueueToast(pkg, mToken, tn, mDuration, displayId);
}
} catch (RemoteException e) {
// Empty
}
}

这里是 enqueueToast 方法中,最后调用:


private void enqueueToast(String pkg, IBinder token, @Nullable CharSequence text,
@Nullable ITransientNotification callback, int duration, int displayId,
@Nullable ITransientNotificationCallback textCallback) {
// ...
record = getToastRecord(callingUid, callingPid, pkg, token, text, callback, duration, windowToken, displayId, textCallback);
// ...
}

getToastRecord 中根据 callback 是否为空产生了不同的 Toast :


private ToastRecord getToastRecord(int uid, int pid, String packageName, IBinder token,
@Nullable CharSequence text, @Nullable ITransientNotification callback, int duration,
Binder windowToken, int displayId,
@Nullable ITransientNotificationCallback textCallback) {
if (callback == null) {
return new TextToastRecord(this, mStatusBar, uid, pid, packageName, token, text,duration, windowToken, displayId, textCallback);
} else {
return new CustomToastRecord(this, uid, pid, packageName, token, callback, duration, windowToken, displayId);
}
}

两者的区别是展示对象的不同:




  • TextToastRecord 因为 ITransientNotification 为空,所以它是通过 mStatusBar 进行展示的:


        @Override
    public boolean show() {
    if (DBG) {
    Slog.d(TAG, "Show pkg=" + pkg + " text=" + text);
    }
    if (mStatusBar == null) {
    Slog.w(TAG, "StatusBar not available to show text toast for package " + pkg);
    return false;
    }
    mStatusBar.showToast(uid, pkg, token, text, windowToken, getDuration(), mCallback);
    return true;
    }



  • CustomToastRecord 调用 ITransientNotification 的 show 方法:


        @Override
    public boolean show() {
    if (DBG) {
    Slog.d(TAG, "Show pkg=" + pkg + " callback=" + callback);
    }
    try {
    callback.show(windowToken);
    return true;
    } catch (RemoteException e) {
    Slog.w(TAG, "Object died trying to show custom toast " + token + " in package "
    + pkg);
    mNotificationManager.keepProcessAliveForToastIfNeeded(pid);
    return false;
    }
    }

    这个 callback 最在 Toast.show() 时传进去的 TN :


    TN tn = mTN;
    service.enqueueToast(pkg, mToken, tn, mDuration, displayId);

    也就是调用到了 TN 的 show 方法:


            @Override
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
    public void show(IBinder windowToken) {
    if (localLOGV) Log.v(TAG, "SHOW: " + this);
    mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
    }




TN 的 show 方法中通过 mHandler 来传递了一个类型是 SHOW 的消息:


            mHandler = new Handler(looper, null) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW: {
IBinder token = (IBinder) msg.obj;
handleShow(token);
break;
}
case HIDE: {
handleHide();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
mNextView = null;
break;
}
case CANCEL: {
handleHide();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
mNextView = null;
try {
getService().cancelToast(mPackageName, mToken);
} catch (RemoteException e) {
}
break;
}
}
}
};

而这个 Handler 在处理 SHOW 时,会调用 handleShow(token) 这个方法里面也就是会触发 ToastPresenter 的 show 方法的地方:


public void handleShow(IBinder windowToken) {
// If a cancel/hide is pending - no need to show - at this point
// the window token is already invalid and no need to do any work.
if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
return;
}
if (mView != mNextView) {
// remove the old view if necessary
handleHide();
mView = mNextView;
// 【here】
mPresenter.show(mView, mToken, windowToken, mDuration, mGravity, mX, mY, mHorizontalMargin, mVerticalMargin, new CallbackBinder(getCallbacks(), mHandler));
}
}

本章节最开始介绍到了 ToastPresenter 的 show 方法中会调用 trySendAccessibilityEvent 方法,也就是从这个方法发送类型是 TYPE_NOTIFICATION_STATE_CHANGED 的无障碍消息给无障碍服务的。


NotificationManagerService


在通知流程中,是通过 NMS 中的 sendAccessibilityEvent 方法来向无障碍发送消息的:


void sendAccessibilityEvent(Notification notification, CharSequence packageName) {
if (!mAccessibilityManager.isEnabled()) {
return;
}

AccessibilityEvent event =
AccessibilityEvent.obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
event.setPackageName(packageName);
event.setClassName(Notification.class.getName());
event.setParcelableData(notification);
CharSequence tickerText = notification.tickerText;
if (!TextUtils.isEmpty(tickerText)) {
event.getText().add(tickerText);
}

mAccessibilityManager.sendAccessibilityEvent(event);
}

这个方法的调用有两处,均在 NMS 的 buzzBeepBlinkLocked 方法中,buzzBeepBlinkLocked 方法是用来处理通知是否应该发出铃声、震动或闪烁 LED 的。省略无关逻辑:


int buzzBeepBlinkLocked(NotificationRecord record) {
// ...
if (!record.isUpdate && record.getImportance() > IMPORTANCE_MIN && !suppressedByDnd) {
sendAccessibilityEvent(notification, record.getSbn().getPackageName());
sentAccessibilityEvent = true;
}

if (aboveThreshold && isNotificationForCurrentUser(record)) {
if (mSystemReady && mAudioManager != null) {
// ...
if (hasAudibleAlert && !shouldMuteNotificationLocked(record)) {
if (!sentAccessibilityEvent) {
sendAccessibilityEvent(notification, record.getSbn().getPackageName());
sentAccessibilityEvent = true;
}
// ...
} else if ((record.getFlags() & Notification.FLAG_INSISTENT) != 0) {
hasValidSound = false;
}
}
}
// ...
}

buzzBeepBlinkLocked 的调用路径有两个:




  • handleRankingReconsideration 方法中 RankingHandlerWorker (这是一个 Handler)调用 handleMessage 处理 MESSAGE_RECONSIDER_RANKING 类型的消息:


    @Override
    public void handleMessage(Message msg) {
    switch (msg.what) {
    case MESSAGE_RECONSIDER_RANKING:
    handleRankingReconsideration(msg);
    break;
    case MESSAGE_RANKING_SORT:
    handleRankingSort();
    break;
    }
    }

    handleRankingReconsideration 方法中调用了 buzzBeepBlinkLocked :


    private void handleRankingReconsideration(Message message) {
    // ...
    synchronized (mNotificationLock) {
    // ...
    if (interceptBefore && !record.isIntercepted()
    && record.isNewEnoughForAlerting(System.currentTimeMillis())) {
    buzzBeepBlinkLocked(record);
    }
    }
    if (changed) {
    mHandler.scheduleSendRankingUpdate();
    }
    }



  • PostNotificationRunnable 的 run 方法。




PostNotificationRunnable


这个东西是用来发送通知并进行处理的,例如提示和重排序等。


PostNotificationRunnable 的构建和 post 在 EnqueueNotificationRunnable 中。在 EnqueueNotificationRunnable 的 run 最后,进行了 post:


public void run() {
// ...
// tell the assistant service about the notification
if (mAssistants.isEnabled()) {
mAssistants.onNotificationEnqueuedLocked(r);
mHandler.postDelayed(new PostNotificationRunnable(r.getKey()), DELAY_FOR_ASSISTANT_TIME);
} else {
mHandler.post(new PostNotificationRunnable(r.getKey()));
}
}

EnqueueNotificationRunnable 在 enqueueNotificationInternal 方法中使用,enqueueNotificationInternal 方法是 INotificationManager 接口中定义的方法,它的实现在 NotificationManager 中:


    public void notifyAsPackage(@NonNull String targetPackage, @Nullable String tag, int id,
@NonNull Notification notification) {
INotificationManager service = getService();
String sender = mContext.getPackageName();

try {
if (localLOGV) Log.v(TAG, sender + ": notify(" + id + ", " + notification + ")");
service.enqueueNotificationWithTag(targetPackage, sender, tag, id,
fixNotification(notification), mContext.getUser().getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}

@UnsupportedAppUsage
public void notifyAsUser(String tag, int id, Notification notification, UserHandle user)
{
INotificationManager service = getService();
String pkg = mContext.getPackageName();

try {
if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")");
service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
fixNotification(notification), user.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}

一般发送一个通知都是通过 NotificationManager 或 NotificationManagerCompat 来发送的,例如:


NotificationManagerCompat.from(this).notify(1, builder.build());

NotificationManagerCompat 中的 notify 方法本质上调用的是 NotificationManager:


// NotificationManagerCompat
public void notify(int id, @NonNull Notification notification) {
notify(null, id, notification);
}

public void notify(@Nullable String tag, int id, @NonNull Notification notification) {
if (useSideChannelForNotification(notification)) {
pushSideChannelQueue(new NotifyTask(mContext.getPackageName(), id, tag, notification));
// Cancel this notification in notification manager if it just transitioned to being side channelled.
mNotificationManager.cancel(tag, id);
} else {
mNotificationManager.notify(tag, id, notification);
}
}

mNotificationManager.notify(tag, id, notification) 中的实现:


public void notify(String tag, int id, Notification notification) {
notifyAsUser(tag, id, notification, mContext.getUser());
}

public void cancel(@Nullable String tag, int id) {
cancelAsUser(tag, id, mContext.getUser());
}

串起来了,最终就是通过 NotificationManager 的 notify 相关方法发送通知,然后触发了通知是否要触发铃声/震动/LED 闪烁的逻辑,并且在这个逻辑中,发送出了无障碍消息。


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

监控主线程耗时操作,从开发中解决ANR

ANR
背景:在 debug 环境中主线程中方法体执行的时间与指定的时间做对比后的堆栈信息,针对性的优化超过指定时间的耗时方法体,减少 ANR 的发生此工具类主要是通过向主线程Looper打印超过指定时间的耗时堆栈信息以及耗时时...
继续阅读 »

背景:

在 debug 环境中主线程中方法体执行的时间与指定的时间做对比后的堆栈信息,针对性的优化超过指定时间的耗时方法体,减少 ANR 的发生

此工具类主要是通过向主线程Looper打印超过指定时间的耗时堆栈信息以及耗时时长,其中校验时间自已定义,主动查看主线程中的耗时操作,防患未然。

原理:

此工具类为最简单最直接处理、优化耗时操作的工具

大家都知道Android 对于ANR的判断标准:

最简单的一句话就是:ANR——应用无响应,Activity是5秒,BroadCastReceiver是10秒,Service是20秒

然后此工具类的方案就是将主线程的堆栈信息作时间对比监控,超时的打印出来

Looper.loop 解析:

  1. 应用之所以未退出,就是运行在loop 中,如果有阻塞loop 的操作就会发生ANR、崩溃
public static void loop() {
final Looper me = myLooper();
//....
for (;;) {
if (!loopOnce(me, ident, thresholdOverride)) {
return;
}
}
}
  1. 主要看死循环

loopOnce

private static boolean loopOnce(final Looper me,
final long ident, final int thresholdOverride) {
Message msg = me.mQueue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return false;
}

// This must be in a local variable, in case a UI event sets the logger
// *当有任务的时候打印Dispatching to *
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " "
+ msg.callback + ": " + msg.what);
}
//.... 中间部分未任务执行的代码

//执行结束之后打印 Finished to
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}

// Make sure that during the course of dispatching the
// identity of the thread wasn't corrupted.
final long newIdent = Binder.clearCallingIdentity();
if (ident != newIdent) {
Log.wtf(TAG, "Thread identity changed from 0x"
+ Long.toHexString(ident) + " to 0x"
+ Long.toHexString(newIdent) + " while dispatching to "
+ msg.target.getClass().getName() + " "
+ msg.callback + " what=" + msg.what);
}

msg.recycleUnchecked();

return true;
}
  1. 上述注释之间的耗时就是主线程在执行某个任务时的耗时,我们只要拿这个时间和指定时间相比就能监控主线程的耗时堆栈信息了

使用方式:

  1. Application:
 //主线程中方法体执行的时间与指定的时间做对比后的堆栈信息,针对性的优化超过指定时间的耗时方法体,
MainThreadDoctor.init(500)
  1. 查看日志:

image.png

日志等级为明显起见使用error级别

工具类:

 /**
* @author kong
* @date 2022/7/6 15:55
* @description 在debug环境中主线程中方法体执行的时间与指定的时间做对比后的堆栈信息,针对性的优化超过指定时间的耗时方法体,减少ANR的发生
**/
object MainThreadDoctor {

private var startTime = 0L
private var currentJob: Job? = null
private const val START = ">>>>> Dispatching"
private const val END = "<<<<< Finished"

fun init(diagnoseStandardTime: Long) {
if (BuildConfigs.DEBUG) {
diagnoseFromMainThread(diagnoseStandardTime)
}
}

/**
* @param diagnoseStandardTime 执行诊断的标准时间
*/
fun diagnoseFromMainThread(diagnoseStandardTime: Long) {
Looper.getMainLooper().setMessageLogging {
if (it.startsWith(START)) {
startTime = System.currentTimeMillis()
currentJob = GlobalScope.launch(Dispatchers.IO) {
delay(diagnoseStandardTime)
val stackTrace = Looper.getMainLooper().thread.stackTrace
val builder = StringBuilder()
for (s in stackTrace) {
builder.append(s.toString())
builder.append("\n")
}
PPLog.e("looperMessageMain $builder")
}
}

if (it.startsWith(END)) {
if (currentJob?.isCompleted == false) {
currentJob?.cancel()
} else {
PPLog.e("looperMessageMain 总时间 = ${System.currentTimeMillis() - startTime} 毫秒")
}
}
}
}
}


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

收起阅读 »

听说Compose与RecyclerView结合会有水土不服?

背景&笔者碎碎谈 最近Compose也慢慢火起来了,作为google力推的ui框架,我们也要用起来才能进步呀!在最新一期的评测中LazyRow等LazyXXX列表组件已经慢慢逼近RecyclerView的性能了!但是还是有很多同学顾虑呀!没关系,我们就...
继续阅读 »

背景&笔者碎碎谈


最近Compose也慢慢火起来了,作为google力推的ui框架,我们也要用起来才能进步呀!在最新一期的评测中LazyRow等LazyXXX列表组件已经慢慢逼近RecyclerView的性能了!但是还是有很多同学顾虑呀!没关系,我们就算用原有的view开发体系,也可以快速迁移到compose,这个利器就是ComposeView了,那么我们在RecyclerView的基础上,集成Compose用起来!这样我们有RecyclerView的性能又有Compose的好处不是嘛!相信很多人都有跟我一样的想法,但是这两者结合起来可是有隐藏的性能开销!(本次使用compose版本为1.1.1)


在原有view体系接入Compose


在纯compose项目中,我们都会用setContent代替原有view体系的setContentView,比如


setContent {
ComposeTestTheme {
// A surface container using the 'background' color from the theme
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
Greeting("Android")
Hello()
}
}
}

那么setContent到底做了什么事情呢?我们看下源码


public fun ComponentActivity.setContent(
parent: CompositionContext? = null,
content: @Composable () -> Unit
) {
val existingComposeView = window.decorView
.findViewById<ViewGroup>(android.R.id.content)
.getChildAt(0) as? ComposeView

if (existingComposeView != null) with(existingComposeView) {
setParentCompositionContext(parent)
setContent(content)
} else ComposeView(this).apply {
// 第一步走到这里
// Set content and parent **before** setContentView
// to have ComposeView create the composition on attach
setParentCompositionContext(parent)
setContent(content)
// Set the view tree owners before setting the content view so that the inflation process
// and attach listeners will see them already present
setOwners()
setContentView(this, DefaultActivityContentLayoutParams)
}
}

由于是第一次进入,那么一定就走到了else分支,其实就是创建了一个ComposeView,放在了android.R.id.content里面的第一个child中,这里就可以看到,compose并不是完全脱了原有的view体系,而是采用了移花接木的方式,把compose体系迁移了过来!ComposeView就是我们能用Compose的前提啦!所以在原有的view体系中,我们也可以通过ComposeView去“嫁接”到view体系中,我们举个例子


class CustomActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_custom)
val recyclerView = this.findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.adapter = MyRecyclerViewAdapter()
recyclerView.layoutManager = LinearLayoutManager(this)
}
}


class MyRecyclerViewAdapter:RecyclerView.Adapter<MyComposeViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyComposeViewHolder {
val view = ComposeView(parent.context)
return MyComposeViewHolder(view)
}

override fun onBindViewHolder(holder: MyComposeViewHolder, position: Int) {
holder.composeView.setContent {
Text(text = "test $position", modifier = Modifier.size(200.dp).padding(20.dp), textAlign = TextAlign.Center)
}

}

override fun getItemCount(): Int {
return 200
}
}

class MyComposeViewHolder(val composeView:ComposeView):RecyclerView.ViewHolder(composeView){

}

这样一来,我们的compose就被移到了RecyclerView中,当然,每一列其实就是一个文本。嗯!普普通通,好像也没啥特别的对吧,假如这个时候你打开了profiler,当我们向下滑动的时候,会发现内存会慢慢的往上浮动


image.png
滑动嘛!有点内存很正常,毕竟谁不生成对象呢,但是这跟我们平常用RecyclerView的时候有点差异,因为RecyclerView滑动的涨幅可没有这个大,那究竟是什么原因导致的呢?


探究Compose


有过对Compose了解的同学可能会知道,Compose的界面构成会有一个重组的过程,当然!本文就不展开聊重组了,因为这类文章有挺多的(填个坑,如果有机会就填),我们聊点特别的,那么什么时候停止重组呢?或者说什么时候这个Compose被dispose掉,即不再参与重组!


Dispose策略


其实我们的ComposeView,以1.1.1版本为例,其实创建的时候,也创建了取消重组策略,即


@Suppress("LeakingThis")
private var disposeViewCompositionStrategy: (() -> Unit)? =
ViewCompositionStrategy.DisposeOnDetachedFromWindow.installFor(this)

这个策略是什么呢?我们点进去看源码


object DisposeOnDetachedFromWindow : ViewCompositionStrategy {
override fun installFor(view: AbstractComposeView): () -> Unit {
val listener = object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {}

override fun onViewDetachedFromWindow(v: View?) {
view.disposeComposition()
}
}
view.addOnAttachStateChangeListener(listener)
return { view.removeOnAttachStateChangeListener(listener) }
}
}

看起来是不是很简单呢,其实就加了一个监听,在onViewDetachedFromWindow的时候调用的view.disposeComposition(),声明当前的ComposeView不参与接下来的重组过程了,我们再继续看


fun disposeComposition() {
composition?.dispose()
composition = null
requestLayout()
}

再看dispose方法


override fun dispose() {
synchronized(lock) {
if (!disposed) {
disposed = true
composable = {}
val nonEmptySlotTable = slotTable.groupsSize > 0
if (nonEmptySlotTable || abandonSet.isNotEmpty()) {
val manager = RememberEventDispatcher(abandonSet)
if (nonEmptySlotTable) {
slotTable.write { writer ->
writer.removeCurrentGroup(manager)
}
applier.clear()
manager.dispatchRememberObservers()
}
manager.dispatchAbandons()
}
composer.dispose()
}
}
parent.unregisterComposition(this)
}

那么怎么样才算是不参与接下里的重组呢,其实就是这里


slotTable.write { writer ->
writer.removeCurrentGroup(manager)
}

...
composer.dispose()

而removeCurrentGroup其实就是把当前的group移除了


for (slot in groupSlots()) {
when (slot) {
....
is RecomposeScopeImpl -> {
val composition = slot.composition
if (composition != null) {
composition.pendingInvalidScopes = true
slot.composition = null
}
}
}
}

这里又多了一个概念,slottable,我们可以这么理解,这里面就是Compose的快照系统,其实就相当于对应着某个时刻view的状态!之所以Compose是声明式的,就是通过slottable里的slot去判断,如果最新的slot跟前一个slot不一致,就回调给监听者,实现更新!这里又是一个大话题了,我们点到为止


image.png


跟RecyclerView有冲突吗


我们看到,默认的策略是当view被移出当前的window就不参与重组了,嗯!这个在99%的场景都是有效的策略,因为你都看不到了,还重组干嘛对吧!但是这跟我们的RecyclerView有什么冲突吗?想想看!诶,RecyclerView最重要的是啥,Recycle呀,就是因为会重复利用holder,间接重复利用了view才显得高效不是嘛!那么问题就来了


image.png
如图,我们item5其实完全可以利用item1进行显示的对不对,差别就只是Text组件的文本不一致罢了,但是我们从上文的分析来看,这个ComposeView对应的composition被回收了,即不参与重组了,换句话来说,我们Adapter在onBindViewHolder的时候,岂不是用了一个没有compositon的ComposeView(即不能参加重组的ComposeView)?这样怎么行呢?我们来猜一下,那么这样的话,RecyclerView岂不是都要生成新的ComposeView(即每次都调用onCreateViewHolder)才能保证正确?emmm,很有道理,但是却不是的!如果我们把代码跑起来看的话,复用的时候依旧是会调用onBindViewHolder,这就是Compose的秘密了,那么这个秘密在哪呢


override fun onBindViewHolder(holder: MyComposeViewHolder, position: Int) {
holder.composeView.setContent {
Text(text = "test $position", modifier = Modifier.size(200.dp).padding(20.dp), textAlign = TextAlign.Center)
}

}

其实就是在ComposeView的setContent方法中,


fun setContent(content: @Composable () -> Unit) {
shouldCreateCompositionOnAttachedToWindow = true
this.content.value = content
if (isAttachedToWindow) {
createComposition()
}
}

fun createComposition() {
check(parentContext != null || isAttachedToWindow) {
"createComposition requires either a parent reference or the View to be attached" +
"to a window. Attach the View or call setParentCompositionReference."
}
ensureCompositionCreated()
}

最终调用的是


private fun ensureCompositionCreated() {
if (composition == null) {
try {
creatingComposition = true
composition = setContent(resolveParentCompositionContext()) {
Content()
}
} finally {
creatingComposition = false
}
}
}

看到了吗!如果composition为null,就会重新创建一个!这样ComposeView就完全嫁接到RecyclerView中而不出现问题了!


其他Dispose策略


我们看到,虽然在ComposeView在RecyclerView中能正常运行,但是还存在缺陷对不对,因为每次复用都要重新创建一个composition对象是不是!归根到底就是,我们默认的dispose策略不太适合这种拥有复用逻辑或者自己生命周期的组件使用,那么有其他策略适合RecyclerView吗?别急,其实是有的,比如DisposeOnViewTreeLifecycleDestroyed


object DisposeOnViewTreeLifecycleDestroyed : ViewCompositionStrategy {
override fun installFor(view: AbstractComposeView): () -> Unit {
if (view.isAttachedToWindow) {
val lco = checkNotNull(ViewTreeLifecycleOwner.get(view)) {
"View tree for $view has no ViewTreeLifecycleOwner"
}
return installForLifecycle(view, lco.lifecycle)
} else {
// We change this reference after we successfully attach
var disposer: () -> Unit
val listener = object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View?) {
val lco = checkNotNull(ViewTreeLifecycleOwner.get(view)) {
"View tree for $view has no ViewTreeLifecycleOwner"
}
disposer = installForLifecycle(view, lco.lifecycle)

// Ensure this runs only once
view.removeOnAttachStateChangeListener(this)
}

override fun onViewDetachedFromWindow(v: View?) {}
}
view.addOnAttachStateChangeListener(listener)
disposer = { view.removeOnAttachStateChangeListener(listener) }
return { disposer() }
}
}
}

然后我们在ViewHolder的init方法中对composeview设置一下就可以了


class MyComposeViewHolder(val composeView:ComposeView):RecyclerView.ViewHolder(composeView){
init {
composeView.setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
}
}

为什么DisposeOnViewTreeLifecycleDestroyed更加适合呢?我们可以看到在onViewAttachedToWindow中调用了
installForLifecycle(view, lco.lifecycle) 方法,然后就removeOnAttachStateChangeListener,保证了该ComposeView创建的时候只会被调用一次,那么removeOnAttachStateChangeListener又做了什么呢?


val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) {
view.disposeComposition()
}
}
lifecycle.addObserver(observer)
return { lifecycle.removeObserver(observer) }

可以看到,是在对应的生命周期事件为ON_DESTROY(Lifecycle.Event跟activity生命周期不是一一对应的,要注意)的时候,才调用view.disposeComposition(),本例子的lifecycleOwner就是CustomActivity啦,这样就保证了只有当前被lifecycleOwner处于特定状态的时候,才会销毁,这样是不是就提高了compose的性能了!


扩展


我们留意到了Compose其实存在这样的小问题,那么如果我们用了其他的组件类似RecyclerView这种的怎么办,又或者我们的开发没有读过这篇文章怎么办!(ps:看到这里的同学还不点赞点赞),没关系,官方也注意到了,并且在1.3.0-alpha02以上版本添加了更换了默认策略,我们来看一下


val Default: ViewCompositionStrategy
get() = DisposeOnDetachedFromWindowOrReleasedFromPool

object DisposeOnDetachedFromWindowOrReleasedFromPool : ViewCompositionStrategy {
override fun installFor(view: AbstractComposeView): () -> Unit {
val listener = object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {}

override fun onViewDetachedFromWindow(v: View) {
// 注意这里
if (!view.isWithinPoolingContainer) {
view.disposeComposition()
}
}
}
view.addOnAttachStateChangeListener(listener)

val poolingContainerListener = PoolingContainerListener { view.disposeComposition() }
view.addPoolingContainerListener(poolingContainerListener)

return {
view.removeOnAttachStateChangeListener(listener)
view.removePoolingContainerListener(poolingContainerListener)
}
}
}

DisposeOnDetachedFromWindow从变成了DisposeOnDetachedFromWindowOrReleasedFromPool,其实主要变化点就是一个view.isWithinPoolingContainer = false,才会进行dispose,isWithinPoolingContainer定义如下


image.png


也就是说,如果我们view的祖先存在isPoolingContainer = true的时候,就不会进行dispose啦!所以说,如果我们的自定义view是这种情况,就一定要把isPoolingContainer变成true才不会有隐藏的性能开销噢!当然,RecyclerView也要同步到1.3.0-alpha02以上才会有这个属性改写!现在稳定版本还是会存在本文的隐藏性能开销,请注意噢!不过相信看完这篇文章,性能优化啥的,不存在了对不对!


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

反对居家办公,马斯克尴尬了:车没地停、工位不够坐、Wi-Fi还太差

每一个特斯拉员工每周都要在办公室工作 40 个小时……如果你不来,那么我们就认为你辞职了。在马斯克“蛮横”地放出了这番话后,迫于失业危机,许多特斯拉员工只能整顿心情并起个大早,只为及时到达办公室——位于美国加利福尼亚州弗里蒙特的特斯拉工厂。万万没想到,员工做好...
继续阅读 »


每一个特斯拉员工每周都要在办公室工作 40 个小时……如果你不来,那么我们就认为你辞职了。

在马斯克“蛮横”地放出了这番话后,迫于失业危机,许多特斯拉员工只能整顿心情并起个大早,只为及时到达办公室——位于美国加利福尼亚州弗里蒙特的特斯拉工厂。

万万没想到,员工做好了返回办公室上班的准备,特斯拉自己却状况百出。

1 车没地停、工位不够、Wi-Fi 太差

首先是停车位问题。很多特斯拉员工花费数小时驱车赶往公司后,面临的第一个问题就是:停车位早已爆满。转悠了好几圈之后,他们只能无奈选择把车停在附近的 BART(一种有轨捷运交通)车站,再转车去上班。

好不容易历经周折到达公司后,看到工位上乌泱泱地挤满了同受马斯克“威胁”来上班的人,员工们再次眉头一紧:我们坐哪里办公?据了解,特斯拉曾在疫情期间将弗里蒙特工厂进行了一些区域调动,新员工不知所措的同时,老员工也一片茫然。

当部分员工幸运地在角落找到位置坐下、以为终于可以开始投入工作后,没想到居然还有一道坎:Wi-Fi 信号太差,差到大多数员工根本无法正常工作。

……就很想问马斯克一句:你当初在威胁员工来公司上班时,是不是忘记了你公司的员工人数早在疫情期间已经翻一番了?

根据外媒 The Information 的数据,从 2019 年到 2021 年,即特斯拉允许远程办公后,其员工人数已增加至 99210 名员工,几乎翻了一番,其中光弗里蒙特工厂就有 2 万多名员工。

在此情形下,即便马斯克一心要求,但由于特斯拉目前无法有效处理因重返办公司政策引发的一系列问题,有些员工被要求每周来公司上班的天数不得超过 5 天。

然而,结合特斯拉最近的裁员计划,这一安排又令许多人心生担忧。

2 暂停全球招聘,计划裁员 10%

本月初,在马斯克发布要求员工每周到岗办公 40 小时的通知后,又在 6 月 2 日向公司高管发送了一封名为“暂停全球所有招聘”的电子邮件。据路透社报道,马斯克在邮件中表示,他对经济形势“感觉非常糟糕”,需要裁员约 10%,并暂停全球招聘。

而上周,一些特斯拉前员工宣布起诉,称特斯拉要裁员 10% 的决定违反了联邦法律,因为没有提供裁员所需的提前通知,即“警告法案”(WARN Act),其中要求公司在任何影响到 50 名或以上员工的大规模裁员前需提前 60 天通知。

据起诉书称,在被裁的 500 多名特斯拉员工中,John Lynch 在 6 月 10 日收到被解雇的通知,立即生效,Daxton Hartsfield 也在 6 月 15 日收到即刻生效的解雇通知。

因而,面临这次的裁员风波,目前在职的特斯拉员工大多都牟足了劲,想凭借勤勉优秀、不出错的表现躲过这次大规模裁员。实际上,马斯克之前在推特全体大会上曾透露,到岗办公并非强制要求,一些“优秀”人才仍然可以选择远程工作。

只是,对于这个毫无具体说明的“优秀”标准,大多数员工都不敢赌,因此应马斯克要求选择到岗办公的员工人数自然也就暴涨——但从目前的情况来看,特斯拉显然没有做好同时容纳这么多到岗员工的准备。

参考链接:

本文转自公众号“CSDN”,ID:CSDNnews

收起阅读 »

插件化工程R文件瘦身技术方案

随着业务的发展及版本迭代,客户端工程中不断增加新的业务逻辑、引入新的资源,随之而来的问题就是安装包体积变大,前期各个业务模块通过无用资源删减、大图压缩或转上云、AB实验业务逻辑下线或其他手段在降低包体积上取得了一定的成果。在瘦身的过程中我们关注到了R文件瘦身的...
继续阅读 »

随着业务的发展及版本迭代,客户端工程中不断增加新的业务逻辑、引入新的资源,随之而来的问题就是安装包体积变大,前期各个业务模块通过无用资源删减、大图压缩或转上云、AB实验业务逻辑下线或其他手段在降低包体积上取得了一定的成果。

在瘦身的过程中我们关注到了R文件瘦身的概念,目前京东APP是支持插件化的,有业务插件工程、宿主工程,对业务插件包文件进行分析,发现除了常规的资源及代码外,R类文件大概占包体积的3%~5%左右,对宿主工程包文件进行分析,R类文件占比也有3%左右。我们先后在对R类文件瘦身的可行性及业界开源项目进行调研后,探索出了一套适用于插件化工程的R文件瘦身技术方案。

理论基础—R文件

R文件也就是我们日常工作中经常打交道的R.java文件,在Android开发规范中我们需要将应用中用到的资源分别放入专门命名的资源目录中,外部化应用资源以便对其进行单独维护。


外部化应用资源后,我们可在项目中使用R类ID来访问这些资源,且R类ID具有唯一性。

public class MainActivity extends BaseActivity {
  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
  }
}

在android apk打包流程中R类文件是由aapt(Android Asset Packaing Tool)工具打包生成的,在生成R类文件的同时对资源文件进行编译,生成resource.arsc文件,resource.arsc文件相当于一个文件索引表,应用层代码通过R类ID 可以访问到对应的资源。


R文件瘦身的可行性分析

日常开发阶段,在主工程中通过R.xx.xx的方式引用资源,经过编译后R类引用对应的常量会被编译进class中。

setContentView(2131427356);

这种变化叫做内联,内联是java的一种机制(如果一个常量被标记为static final,在java编译的过程中会将常量内联到代码中,减少一次变量的内存寻址)。

非主工程中,R类资源ID以引用的方式编译进class中,不会产生内联。

setContentView(R.layout.activity_main);

产生这种现象的原因是AGP打包工具导致的。具体细节,大家可以去查阅一下android gradle plugin在R文件上的处理过程。

结论:R类id内联后程序可运行,但并非所有的工程都会自动产生内联现象,我们需要通过技术手段在合适的时机将R类id内联到程序中,内联完成后,由于不再依赖R类文件,则可以将R类文件删除,在应用正常运行的同时,达到包瘦身目的。

插件化工程R文件瘦身实战

制定技术方案

目前京东Android客户端是支持插件化的,整个插件化工程包含公共库(是一个aar工程,用来存放组件和宿主共用的类和资源)、业务插件(插件工程是一个独立的工程,编译产物可以运行在宿主环境中)、宿主(主工程,提供运行环境)。在插件化的过程中为了防止宿主和插件资源冲突,通过修改插件packageId保证了资源的唯一性。由于公共资源库、宿主是被很多业务依赖,对这两个项目进行改动评估影响涉及比较多,插件一般都是业务模块自行维护,不存在被依赖问题,所以先在业务插件模块进行R类瘦身实践。

对业务插件工程打出的包进行反编译以后,发现R类ID无内联现象,且R类文件具有一定的大小,对包内的R文件进行分析,发现R文件中仅包含业务自身的资源,不包含业务依赖的公共资源R类。

public View onCreateView(LayoutInflater paramLayoutInflater, ViewGroup paramViewGroup, Bundle paramBundle)
{
  this.b = paramLayoutInflater.inflate(R.layout.lib_pd_main_page, paramViewGroup, false);
  this.h = (PDBuyStatusView) this.b.findViewById(R.id.pd_buy_status_view);
  this.f = (PageRecyclerView) this.b.findViewById(R.id.lib_pd_recycle_view);
}


结合对业界开源项目的调研分析,尝试制定符合京东商城的技术方案并优先在业务插件内完成R类ID内联并删除对应的R文件。

1.通过**transform** api 收集要处理的class文件

Transform 是 Android Gradle 提供的操作字节码的一种方式,它在 class 编译成 dex 之前通过一系列 Transform 处理来实现修改.class文件。

@Override
public void transform(TransformInvocation transformInvocationthrows TransformExceptionInterruptedExceptionIOException {
super.transform(transformInvocation);
// 通过TransformInvocation.getInputs()获取输入文件,有两种
// DirectoryInpu以源码方式参与编译的目录结构及目录下的文件
// JarInput以jar包方式参与编译的所有jar包
   allDirs = new ArrayList<>(invocation.getInputs().size());
   allJars = new ArrayList<>(invocation.getInputs().size());
   Collection<TransformInput> inputs = invocation.getInputs();
   for (TransformInput input : inputs) {
       Collection<DirectoryInput> directoryInputs = input.getDirectoryInputs();
        for (DirectoryInput directoryInput : directoryInputs) {
              allDirs.add(directoryInput.getFile());
            }
           Collection<JarInput> jarInputs = input.getJarInputs();
        for (JarInput jarInput : jarInputs) {
               allJars.add(jarInput.getFile());
            }
    }
}

2.对收集到的.class文件结合ASM框架进行分析处理

ASM是一个操作Java字节码的类库,通过ASM我们可以方便对.class文件进行修改。

优先识别R类文件,通过ClassVisitor访问R.class文件,读取文件中的静态常量,进行临时变量存储:

@Overridepublic FieldVisitor visitField(int access, String name, String desc, String signature, Object value)
{ //R类中收集 
  public static final int 对应的变量
  if(JDASMUtil.isPublic(access) && JDASMUtil.isStatic(access) && JDASMUtil.isFinal(access) && JDASMUtil.isInt(desc))
  {
      jdRstore.addInlineRField(className, name, value);
  }
  return super.visitField(access, name, desc, signature, value);
}

非R类文件,通过MethodVisitor识别到代码中的R类引用,获取引用对应的值,进行id值替换:

@Override
   public void visitFieldInsn(int opcodeString ownerString nameString desc) {
       if (opcode == Opcodes.GETSTATIC) {
           //owner:包名;name:具体变量名;value:R类变量对应的具体id值
           Object value = jdRstore.getRFieldValue(ownername);
           if (value != null) {
             //调用该api实现值替换
               mv.visitLdcInsn(value);
               return;
          }
      }
       super.visitFieldInsn(opcodeownernamedesc);
  }

*注:以上代码仅为部分示意代码,非正式插件代码。


在业务模块引入R类瘦身插件后,业务模块功能可正常运行,且插件包大小均有3%~5%不同程度的减少。

公共资源R类ID内联

由于在京东android客户端代码中,更多的资源文件集中在公共资源库中,相对的公共库生成的R类文件也更大,对编译后的apk包内容进行分析后,公共资源库的R类文件占比高达3%。

公共库跟随宿主一起打包,在宿主打包过程中引入R类瘦身插件,打包后的apk有明显的减小,手机安装apk后启动首页正常展示无问题,但在打开某些业务插件时,会有异常闪退现象,崩溃类型为R.x resource not found。对崩溃原因分析如下:业务插件代码中使用了公共库中的R类资源、插件打包流程独立于宿主打包,在插件打包的过程中仅完成了业务模块R类的内联,并没有考虑到公共资源R类的内联,基于上述原因当宿主打包过程完成R类文件删除瘦身后,我们在运行某业务插件的过程中,自然就会报公共资源R类找不到的问题从而产生崩溃。


为了解决这个问题一开始的方案设想是增加白名单机制,keep住所有被业务模块使用的公共资源,但很快这个想法就被推翻,公共资源存在本身就是希望各个业务模块直接引用这部分资源,而不是自己定义,如果keep住的话,必然有很大一部分的资源无法删减,瘦身的效果会大打折扣。

既然保留的方案并不合适,那就将公共资源R类id也内联到代码中去。前面提到京东是支持插件化的,整个插件化方案是基于aura平台实现的,我们向aura团队进行了咨询,然后get到了新的方案切入点。

aura平台在插件化的过程中已通过aapt2引入了公共资源id固定的能力,在该能力下,已定义的公共资源id会一直固定(各个业务插件中引用的公共资源id一致),且公共资源库中已有的资源不可被其他模块重复定义,否则会覆盖之前已定义好的资源,基于上述的结果和规则,我们对之前的R文件瘦身gralde plugin功能进行完善,将公共资源的R类id 内联到项目中。

利用appt2的-stable-ids和-emit-ids两个参数实现固化资源id的功能,并将将固化后的ids文件命名为shared_res_public.xml存储在公共资源库中,业务插件依赖公共资源库,在打包编译的过程中aura会将shared_res_public.xml复制到业务工程临时编译文件夹intermediates下的指定位置并参与业务模块的打包过程中,其文件内容格式如下:


修改R文件瘦身gradle plugin 代码,从指定位置读取并识别这部分公共资源,按照<name,id>的形式进行变量存储,并在后续过程中对业务模块中的公共资源部分进行id替换。


public Map<StringString> parse() thro ws Exce ption {
       if (in == null) {
           return null;
      }
       DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
       DocumentBuilder builder = factory.newDocumentBuilder();
       Document doc = builder.parse(in);
       Element rootElement = doc.getDocumentElement();
       NodeList list = rootElement.getChildNodes();
      ......
       return resNode;
  }
}

至此,我们的R文件瘦身gradle plugin将R资源分为两部分进行存储,一部分为业务自身的R类资源,一部分为我们解析固定目录下的公共R类资源,对之前的R文件瘦身流程进行如下修改:


R类资源id内联部分代码如下:

public void visitFieldInsn(int opcodeString ownerString nameString desc) {
       if (opcode == Opcodes.GETSTATIC) {
           //优先从业务模块R类资源中查找
           Object value = jdRstore.getRFieldValue(ownername);
           if (value != null) {
               mv.visitLdcInsn(value);
               return;
          }
          //从公共R类资源中查找
           value = getPublicRFileValue(name);
           if (value != null) {
               mv.visitLdcInsn(value);
               return;
          }
      }
       super.visitFieldInsn(opcodeownernamedesc);
  }

该方案完善后,结合商详业务插件进行了验证,在商详及宿主均完成R文件内联瘦身后,商详模块业务功能可正常使用,无异常现象。

考虑到R文件内联瘦身gradle plugin是在打包编译阶段引入的,我们也统计了一下引入该插件以后对打包时长的影响,数据如下:


结合数据来看,引入R文件瘦身插件后对整体打包时长并无显著影响。

至此,基于京东商城探索的插件化工程R文件瘦身gradle plugin就开发完成,目前已在部分业务插件模块进行了线上验证,在功能上线以后我们也及时的进行了崩溃观测以及用户反馈的跟进,暂无异常问题。当然围绕R文件瘦身缩减包体积这个目的,开发人员有各种各样的技术方案,上述方案不一定适用于所有的客户端开发体系,另外后续也将围绕包瘦身这一常态事务建设一系列的相关工具,介入工作当中的各个阶段,高效、有效的控制包体积的增长,如大家在瘦身方面有相关建议和想法也欢迎大家来一起讨论。

参考文章:

Gradle Plugin:

https://docs.gradle.org/current/userguide/custom_plugins.html

Gradle Transform:

https://developer.android.com/reference/tools/gradle-api/7.0/com/android/build/api/transform/Transform

APK 构建流程:

https://developer.android.com/studio/build/index.html?hl=zh-cn#build-process

作者:耿蕾 田创新 京东零售技术

收起阅读 »

DevOps之【持续集成】

引言对于客户或者需求方来说,可以集成交付的软件才是有价值的。每个软件都有集成的过程,如果软件规模比较小,比如只有一个人而且没有外部依赖,那集成没什么问题。随着软件变得复杂,依赖变多,开发人员变多,那么早集成、常集成,就可以尽早暴露问题,做出相应的调整,防止在软...
继续阅读 »

引言

对于客户或者需求方来说,可以集成交付的软件才是有价值的。

每个软件都有集成的过程,如果软件规模比较小,比如只有一个人而且没有外部依赖,那集成没什么问题。随着软件变得复杂,依赖变多,开发人员变多,那么早集成、常集成,就可以尽早暴露问题,做出相应的调整,防止在软件后期才发现问题,从而导致软件失败。

定义

大师Martin Fowler对持续集成是这样定义的:

持续集成是一种软件开发实践,即团队开发成员经常集成它们的工作,通常每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽快地发现集成错误。许多团队发现这个过程可以大大减少集成的问题,让团队能够更快的开发内聚的软件。

优点

  • 降低软件风险 早集成,常集成,并且做了有效的测试,有利于尽早暴露问题和软件缺陷,了解软件的健康情况。假定越少的软件,对于维护和新业务开发都是有利的。 如果botslab每一个品类的设备都单独分支开发,不能及时集成进来,依赖问题、冲突问题、业务复用问题不能尽早解决,问题累积到一定程度解决成本会变大,业务发展是不会为代码重构让路的,那么软件质量的降低是必然的,很多项目都是这样失败的

  • 减少重复过程 软件集成的过程看起来简单,但是做起来难。软件的编译,测试,审查,部署,反馈,这些重复劳动是非常耗时,且没有意义的,自动化集成可以让开发解放出来,做一些用脑袋的事情。 如果出现疏漏,会给下游的参与者带来额外工作量和项目质量的误判。软件的输出质量是会影响项目计划的,所以持续集成很重要的一点就是自动化

  • 随时生成可以部署的软件 持续集成可以随时随地输出可以部署的软件,这一点对于需求方或客户是明显的好处。我们可以对客户说软件有多么好的架构,多么高质量的代码,但是对于客户来说,一个可以使用的软件才是他的实际资产。持续交付可以尽早的暴露产品问题和开发方向,客户才能给出有效的意见和开发重点。

  • 软件是透明的 持续集成会生成软件构建状态和品质信息,经常集成可以看到一些趋势,预测一些软件质量走向。

  • 团队信心 持续集成可以建立团队的信心,开发清楚的知道自己的代码产生了什么影响,测试对软件质量的预测稳定,产品或客户可以放心的需求了

步骤

  1. 统一的代码库

  2. 自动构建

  3. 自动测试

  4. 每个人每天都要向代码库主干提交代码

  5. 每次代码递交后都会在持续集成服务器上触发一次构建

  6. 保证快速构建

  7. 模拟生产环境的自动测试

  8. 每个人都可以很容易的获取最新可执行的应用程序

  9. 每个人都清楚正在发生的状况

  10. 自动化的部署

原则

  1. 所有的开发人员需要在本地机器上做本地构建,然后再提交的版本控制库中,从而确保他们的变更不会导致持续集成失败。

  2. 开发人员每天至少向版本控制库中提交一次代码。

  3. 开发人员每天至少需要从版本控制库中更新一次代码到本地机器。

  4. 需要有专门的集成服务器来执行集成构建,每天要执行多次构建。

  5. 每次构建都要100%通过。

  6. 每次构建都可以生成可发布的产品。

  7. 修复失败的构建是优先级最高的事情。



作者:QiShare
来源:juejin.cn/post/6986884632222384141

收起阅读 »

Vue2全家桶之一:vue-cli

vue
vue.js有著名的全家桶系列,包含了vue-router,vuex, vue-resource,再加上构建工具vue-cli,就是一个完整的vue项目的核心构成。 1.安装vue-cli② 全局安装vue-cli,在cmd中输入命令:安装成功:打开C:\U...
继续阅读 »

都说Vue2简单上手容易,的确,看了官方文档确实觉得上手很快,除了ES6语法和webpack的配置让你感到陌生,重要的是思路的变换,以前用jq随便拿全局变量和修改dom的锤子不能用了,vue只用关心数据本身,不用再频繁繁琐的操作dom,注册事件、监听事件、取消事件。。。。(确实很烦)。vue的官方文档还是不错的,由浅到深,如果不使用构建工具确实用的很爽,但是这在实际项目应用中是不可能的,当用vue-cli构建一个工程的时候,发现官方文档还是不够用,需要熟练掌握es6,而vue的全家桶(vue-cli,vue-router,vue-resource,vuex)还是都要上的。

vue.js有著名的全家桶系列,包含了vue-router,vuex, vue-resource,再加上构建工具vue-cli,就是一个完整的vue项目的核心构成。

vue-cli这个构建工具大大降低了webpack的使用难度,支持热更新,有webpack-dev-server的支持,相当于启动了一个请求服务器,给你搭建了一个测试环境,只关注开发就OK。

1.安装vue-cli

使用npm(需要安装node环境)全局安装webpack,打开命令行工具输入:npm install webpack -g或者(npm install -g webpack),安装完成之后输入 webpack -v,如下图,如果出现相应的版本号,则说明安装成功。

全局安装vue-cli,在cmd中输入命令:

npm install --global vue-cli

安装成功:



安装完成之后输入 vue -V(注意这里是大写的“V”),如下图,如果出现相应的版本号,则说明安装成功。

打开C:\Users\Andminster\AppData\Roaming\npm目录下可以看到:



打开node_modules也可以看到:

2.用vue-cli来构建项目

① 我首先在D盘新建一个文件夹(dxl_vue)作为项目存放地,然后使用命令行cd进入到项目目录输入:

vue init webpack baoge

baoge是自定义的项目名称,命令执行之后,会在当前目录生成一个以该名称命名的项目文件夹。
输入命令后,会跳出几个选项让你回答:

  • Project name (baoge): -----项目名称,直接回车,按照括号中默认名字(注意这里的名字不能有大写字母,如果有会报错Sorry, name can no longer contain capital letters),阮一峰老师博客为什么文件名要小写 ,可以参考一下。
  • Project description (A Vue.js project): ----项目描述,也可直接点击回车,使用默认名字
  • Author (): ----作者,输入dongxili
    接下来会让用户选择:
  • Runtime + Compiler: recommended for most users 运行加编译,既然已经说了推荐,就选它了
    Runtime-only: about 6KB lighter min+gzip, but templates (or any Vue-specificHTML) are ONLY allowed in .vue files - render functions are required elsewhere 仅运行时,已经有推荐了就选择第一个了
  • Install vue-router? (Y/n) 是否安装vue-router,这是官方的路由,大多数情况下都使用,这里就输入“y”后回车即可。
  • Use ESLint to lint your code? (Y/n) 是否使用ESLint管理代码,ESLint是个代码风格管理工具,是用来统一代码风格的,一般项目中都会使用。
    接下来也是选择题Pick an ESLint preset (Use arrow keys) 选择一个ESLint预设,编写vue项目时的代码风格,直接y回车
  • Setup unit tests with Karma + Mocha? (Y/n) 是否安装单元测试,我选择安装y回车
  • Setup e2e tests with Nightwatch(Y/n)? 是否安装e2e测试 ,我选择安装y回车

回答完毕后上图就开始构建项目了。

配置完成后,可以看到目录下多出了一个项目文件夹baoge,然后cd进入这个文件夹:
安装依赖

npm install

 ( 如果安装速度太慢。可以安装淘宝镜像,打开命令行工具,输入:
npm install -g cnpm --registry=https://registry.npm.taobao.org
 然后使用cnpm来安装 )

npm install :安装所有的模块,如果是安装具体的哪个个模块,在install 后面输入模块的名字即可。而只输入install就会按照项目的根目录下的package.json文件中依赖的模块安装(这个文件里面是不允许有任何注释的),每个使用npm管理的项目都有这个文件,是npm操作的入口文件。因为是初始项目,还没有任何模块,所以我用npm install 安装所有的模块。安装完成后,目录中会多出来一个node_modules文件夹,这里放的就是所有依赖的模块。


然后现在,baoge文件夹里的目录是这样的:



解释下每个文件夹代表的意思(仔细看一下这张图):

image.png

3.启动项目

npm run dev


如果浏览器打开之后,没有加载出页面,有可能是本地的 8080 端口被占用,需要修改一下配置文件 config里的index.js

还有,如果本地调试项目时,建议将build 里的assetsPublicPath的路径前缀修改为 ' ./ '(开始是 ' / '),因为打包之后,外部引入 js 和 css 文件时,如果路径以 ' / ' 开头,在本地是无法找到对应文件的(服务器上没问题)。所以如果需要在本地打开打包后的文件,就得修改文件路径。
我的端口没有被占用,直接成功(服务启动成功后浏览器会默认打开一个“欢迎页面”):



注意:在进行vue页面调试时,一定要去谷歌商店下载一个vue-tool扩展程序。

4.vue-cli的webpack配置分析

  • package.json可以看到开发和生产环境的入口。

  • 可以看到dev中的设置,build/webpack.dev.conf.js,该文件是开发环境中webpack的配置入口。
  • 在webpack.dev.conf.js中出现webpack.base.conf.js,这个文件是开发环境和生产环境,甚至测试环境,这些环境的公共webpack配置。可以说,这个文件相当重要。
  • 还有config/index.js 、build/utils.js 、build/build.js等,具体请看这篇介绍:
    https://segmentfault.com/a/1190000008644830

5.打包上线

注意,自己的项目文件都需要放到 src 文件夹下。
在项目开发完成之后,可以输入 npm run build 来进行打包工作。

npm run build

另:

1.npm 开启了npm run dev以后怎么退出或关闭?
ctrl+c
2.--save-dev
自动把模块和版本号添加到模块配置文件package.json中的依赖里devdependencies部分
3. --save-dev 与 --save 的区别
--save 安装包信息将加入到dependencies(生产阶段的依赖)
--save-dev 安装包信息将加入到devDependencies(开发阶段的依赖),所以开发阶段一般使用它

打包完成后,会生成 dist 文件夹,如果已经修改了文件路径,可以直接打开本地文件查看。
项目上线时,只需要将 dist 文件夹放到服务器就行了。

转载自: https://cloud.tencent.com/developer/article/1896690

收起阅读 »

[PHP 安全] pcc —— PHP 安全配置检测工具

PHP
背景在 PHP 安全测试中最单调乏味的任务之一就是检查不安全的 PHP 配置项。作为一名 PHP 安全海报的继承者,我们创建了一个脚本用来帮助系统管理员如同安全专家一样尽可能快速且全面地评估 php.ini 和相关主题的状态。在下文中,该脚本被称作“PHP 安...
继续阅读 »

背景

在 PHP 安全测试中最单调乏味的任务之一就是检查不安全的 PHP 配置项。作为一名 PHP 安全海报的继承者,我们创建了一个脚本用来帮助系统管理员如同安全专家一样尽可能快速且全面地评估 php.ini 和相关主题的状态。在下文中,该脚本被称作“PHP 安全配置项检查器”,或者 pcc

https://github.com/sektioneins/pcc

概念

  • 一个便于分发的单文件
  • 有对每个安全相关的 ini 条目的简单测试
  • 包含一些其他测试 - 但不太复杂
  • 兼容 PHP >= 5.4, 或者 >= 5.0
  • 没有复杂/过度设计的代码,例如没有类/接口,测试框架,类库等等。它应该第一眼看上去是显而易见的-甚至对于新手-这个工具怎么使用能用来做什么。
  • 没有(或者少量的)依赖

使用 / 安装

  • CLI:简单调用 php phpconfigcheck.php。然后,添加参数 -a 以便更好的查看隐藏结果, -h 以 HTML 格式输出, -j 以 JSON 格式输出.
  • WEB: 复制这个脚本文件到你的服务器上的任意一个可访问目录,比如 root 目录。参见下面的“防护措施”。
    在非 CLI 模式下默认输出 HTML 格式。可以通过修改设置环境变量PCC_OUTPUT_TYPE=text 或者 PCC_OUTPUT_TYPE=json改变这个行为。
    一些测试用例默认是被隐藏的,特别是skipped、ok和 unknown/untested这些。要显示全部结果,可以用 phpconfigcheck.php?showall=1,但这并不适用于 JSON 输出,它默认返回全部结果。
    在 WEB 模式下控制输出格式用 phpconfigcheck.php?format=...format的值可以是 text, html 或者 json中的一个,例如: phpconfigcheck.php?format=textformat 参数优先于 PCC_OUTPUT_TYPE。

保障措施

大多数情况下,最好是自己来关注与安全性相关的问题比如PHP的配置。脚本已实现下列保障措施:

  • mtime检查:脚本在非CLI环境中只能工作两天。可以通过touch phpconfigcheck.php或者将脚本文件再次复制到你的服务器(例如通过SCP)来重新进行mtime检查。可以通过设置环境量: PCC_DISABLE_MTIME=1,比如在apache的.htaccess文件中设置SetEnv PCC_DISABLE_MTIME 1来禁用mtime检查。
  • 来源IP检查:默认情况下,只有localhost (127.0.0.1 和 ::1)才能访问这个脚本。其他主机可以通过在PCC_ALLOW_IP中添加IP地址或者通配符表达式的方式来访问脚本,比如在.htaccess文件中设置SetEnv PCC_ALLOW_IP 10.0.0.*。你还可以选择通过SSH端口转发访问您的web服务器, 比如 ssh -D 或者 ssh -L

下载

可以通过github下载第一个完整的开发版: https://github.com/sektioneins/pcc

如果有好的建议或者遇到bug请给我们提issue:

截图

HTML输出的列表是根据问题严重性排序的,通过颜色代码的形式列出了所有建议。列表顶部的状态行会显示问题的数量。


转载自: https://cloud.tencent.com/developer/article/1911011

收起阅读 »

Java Exception

异常指不期而至的各种状况,如:文件找不到、网络连接失败、非法参数等。异常是一个事件,它发生在程序运行期间,干扰了正常的指令流程。Java通 过API中Throwable类的众多子类描述各种不同的异常。因而,Java异常都是对象,是Throwable子类的实例,...
继续阅读 »

Java异常

异常指不期而至的各种状况,如:文件找不到、网络连接失败、非法参数等。异常是一个事件,它发生在程序运行期间,干扰了正常的指令流程。Java通 过API中Throwable类的众多子类描述各种不同的异常。因而,Java异常都是对象,是Throwable子类的实例,描述了出现在一段编码中的 错误条件。当条件生成时,错误将引发异常。
Java异常类层次结构图:


图1 Java异常类层次结构图
在 Java 中,所有的异常都有一个共同的祖先 Throwable(可抛出)。Throwable 指定代码中可用异常传播机制通过 Java 应用程序传输的任何问题的共性。 Throwable: 有两个重要的子类:Exception(异常)和 Error(错误),二者都是 Java 异常处理的重要子类,各自都包含大量子类。
Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。
这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java中,错误通过Error的子类描述。
Exception(异常):是程序本身可以处理的异常。
Exception 类有一个重要的子类 RuntimeException。RuntimeException 类及其子类表示“JVM 常用操作”引发的错误。例如,若试图使用空值对象引用、除数为零或数组越界,则分别引发运行时异常(NullPointerException、ArithmeticException)和 ArrayIndexOutOfBoundException。
注意:异常和错误的区别:异常能被程序本身可以处理,错误是无法处理。
通常,Java的异常(包括Exception和Error)分为可查的异常(checked exceptions)和不可查的异常(unchecked exceptions)
可查异常(编译器要求必须处置的异常):正确的程序在运行中,很容易出现的、情理可容的异常状况。可查异常虽然是异常状况,但在一定程度上它的发生是可以预计的,而且一旦发生这种异常状况,就必须采取某种方式进行处理。

除了RuntimeException及其子类以外,其他的Exception类及其子类都属于可查异常。这种异常的特点是Java编译器会检查它,也就是说,当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。

不可查异常(编译器不要求强制处置的异常):包括运行时异常(RuntimeException与其子类)和错误(Error)。

Exception 这种异常分两大类运行时异常和非运行时异常(编译异常)。程序中应当尽可能去处理这些异常。

运行时异常:都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。

运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。 非运行时异常 (编译异常):是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。

4.处理异常机制

在 Java 应用程序中,异常处理机制为:抛出异常,捕捉异常。

抛出异常:当一个方法出现错误引发异常时,方法创建异常对象并交付运行时系统,异常对象中包含了异常类型和异常出现时的程序状态等异常信息。运行时系统负责寻找处置异常的代码并执行。

捕获异常 :在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)。潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适 的异常处理器。运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适 的异常处理器,则运行时系统终止。同时,意味着Java程序的终止。

对于运行时异常、错误或可查异常,Java技术所要求的异常处理方式有所不同。

由于运行时异常的不可查性,为了更合理、更容易地实现应用程序,Java规定,运行时异常将由Java运行时系统自动抛出,允许应用程序忽略运行时异常。

对于方法运行中可能出现的Error,当运行方法不欲捕捉时,Java允许该方法不做任何抛出声明。因为,大多数Error异常属于永远不能被允许发生的状况,也属于合理的应用程序不该捕捉的异常。

对于所有的可查异常,Java规定:一个方法必须捕捉,或者声明抛出方法之外。也就是说,当一个方法选择不捕捉可查异常时,它必须声明将抛出异常。

能够捕捉异常的方法,需要提供相符类型的异常处理器。所捕捉的异常,可能是由于自身语句所引发并抛出的异常,也可能是由某个调用的方法或者Java运行时 系统等抛出的异常。也就是说,一个方法所能捕捉的异常,一定是Java代码在某处所抛出的异常。简单地说,异常总是先被抛出,后被捕捉的。

任何Java代码都可以抛出异常,如:自己编写的代码、来自Java开发环境包中代码,或者Java运行时系统。无论是谁,都可以通过Java的throw语句抛出异常。

从方法中抛出的任何异常都必须使用throws子句。

捕捉异常通过try-catch语句或者try-catch-finally语句实现。

总体来说,Java规定:对于可查异常必须捕捉、或者声明抛出。允许忽略不可查的RuntimeException和Error。

4.1 捕获异常:try、catch 和 finally

1.try-catch语句

在Java中,异常通过try-catch语句捕获。其一般语法形式为:

try {  
// 可能会发生异常的程序代码
} catch (Type1 id1){
// 捕获并处置try抛出的异常类型Type1
}
catch (Type2 id2){
//捕获并处置try抛出的异常类型Type2
}

关键词try后的一对大括号将一块可能发生异常的代码包起来,称为监控区域。Java方法在运行过程中出现异常,则创建异常对象。将异常抛出监控区域之 外,由Java运行时系统试图寻找匹配的catch子句以捕获异常。若有匹配的catch子句,则运行其异常处理代码,try-catch语句结束。

匹配的原则是:如果抛出的异常对象属于catch子句的异常类,或者属于该异常类的子类,则认为生成的异常对象与catch块捕获的异常类型相匹配。

例1 捕捉throw语句抛出的“除数为0”异常。

public class TestException {  
public static void main(String[] args) {
int a = 6;
int b = 0;
try { // try监控区域

if (b == 0) throw new ArithmeticException(); // 通过throw语句抛出异常
System.out.println("a/b的值是:" + a / b);
}
catch (ArithmeticException e) { // catch捕捉异常
System.out.println("程序出现异常,变量b不能为0。");
}
System.out.println("程序正常结束。");
}
}

运行结果:程序出现异常,变量b不能为0。

程序正常结束。

例1 在try监控区域通过if语句进行判断,当“除数为0”的错误条件成立时引发ArithmeticException异常,创建 ArithmeticException异常对象,并由throw语句将异常抛给Java运行时系统,由系统寻找匹配的异常处理器catch并运行相应异 常处理代码,打印输出“程序出现异常,变量b不能为0。”try-catch语句结束,继续程序流程。

事实上,“除数为0”等ArithmeticException,是RuntimException的子类。而运行时异常将由运行时系统自动抛出,不需要使用throw语句。

例2 捕捉运行时系统自动抛出“除数为0”引发的ArithmeticException异常。

public static void main(String[] args) {  
int a = 6;
int b = 0;
try {
System.out.println("a/b的值是:" + a / b);
} catch (ArithmeticException e) {
System.out.println("程序出现异常,变量b不能为0。");
}
System.out.println("程序正常结束。");
}
}

运行结果:程序出现异常,变量b不能为0。
程序正常结束。

例2 中的语句:

System.out.println("a/b的值是:" + a/b);

在运行中出现“除数为0”错误,引发ArithmeticException异常。运行时系统创建异常对象并抛出监控区域,转而匹配合适的异常处理器catch,并执行相应的异常处理代码。

由于检查运行时异常的代价远大于捕捉异常所带来的益处,运行时异常不可查。Java编译器允许忽略运行时异常,一个方法可以既不捕捉,也不声明抛出运行时异常。

例3 不捕捉、也不声明抛出运行时异常。

public class TestException {  
public static void main(String[] args) {
int a, b;
a = 6;
b = 0; // 除数b 的值为0
System.out.println(a / b);
}
} 复制

运行结果:
Exception in thread "main" java.lang.ArithmeticException: / by zero at Test.TestException.main(TestException.java:8)

例4 程序可能存在除数为0异常和数组下标越界异常。

public class TestException {  
public static void main(String[] args) {
int[] intArray = new int[3];
try {
for (int i = 0; i <= intArray.length; i++) {
intArray[i] = i;
System.out.println("intArray[" + i + "] = " + intArray[i]);
System.out.println("intArray[" + i + "]模 " + (i - 2) + "的值: "
+ intArray[i] % (i - 2));
}
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("intArray数组下标越界异常。");
} catch (ArithmeticException e) {
System.out.println("除数为0异常。");
}
System.out.println("程序正常结束。");
}
}

运行结果:

intArray[0] = 0

intArray[0]模 -2的值: 0

intArray[1] = 1

intArray[1]模 -1的值: 0

intArray[2] = 2

除数为0异常。

程序正常结束。

例4 程序可能会出现除数为0异常,还可能会出现数组下标越界异常。程序运行过程中ArithmeticException异常类型是先行匹配的,因此执行相匹配的catch语句:

catch (ArithmeticException e){  
System.out.println("除数为0异常。");
}

需要注意的是,一旦某个catch捕获到匹配的异常类型,将进入异常处理代码。一经处理结束,就意味着整个try-catch语句结束。其他的catch子句不再有匹配和捕获异常类型的机会。

Java通过异常类描述异常类型,异常类的层次结构如图1所示。对于有多个catch子句的异常程序而言,应该尽量将捕获底层异常类的catch子 句放在前面,同时尽量将捕获相对高层的异常类的catch子句放在后面。否则,捕获底层异常类的catch子句将可能会被屏蔽。

RuntimeException异常类包括运行时各种常见的异常,ArithmeticException类和ArrayIndexOutOfBoundsException类都是它的子类。因此,RuntimeException异常类的catch子句应该放在 最后面,否则可能会屏蔽其后的特定异常处理或引起编译错误。

收起阅读 »

Object.defineProperty也能监听数组变化?

首先,解答一下标题:Object.defineProperty 不能监听原生数组的变化。如需监听数组,要将数组转成对象。 在 Vue2 时是使用了 Object.defineProperty 监听数据变化,但我查了下 文档,发现 Object.define...
继续阅读 »


首先,解答一下标题:Object.defineProperty 不能监听原生数组的变化。如需监听数组,要将数组转成对象。




Vue2 时是使用了 Object.defineProperty 监听数据变化,但我查了下 文档,发现 Object.defineProperty 是用来监听对象指定属性的变化。没有看到可以监听个数组变化的。


Vue2 有的确能监听到数组某些方法改变了数组的值。本文的目标就是解开这个结。






基础用法


Object.defineProperty() 文档


关于 Object.defineProperty() 的用法,可以看官方文档。


基础部分本文只做简单的讲解。




语法


Object.defineProperty(obj, prop, descriptor)

参数


  • obj 要定义属性的对象。
  • prop 要定义或修改的属性的名称或 Symbol
  • descriptor 要定义或修改的属性描述符。

const data = {}
let name = '雷猴'

Object.defineProperty(data, 'name', {
get() {
console.log('get')
return name
},
set(newVal) {
console.log('set')
name = newVal
}
})

console.log(data.name)
data.name = '鲨鱼辣椒'

console.log(data.name)
console.log(name)

上面的代码会输出


get
雷猴
set
鲨鱼辣椒
鲨鱼辣椒



上面的意思是,如果你需要访问 data.name ,那就返回 name 的值。


如果你想设置 data.name ,那就会将你传进来的值放到变量 name 里。


此时再访问 data.name 或者 name ,都会返回新赋予的值。




还有另一个基础用法:“冻结”指定属性


const data = {}

Object.defineProperty(data, 'name', {
value: '雷猴',
writable: false
})

data.name = '鲨鱼辣椒'
delete data.name
console.log(data.name)

这个例子,把 data.name 冻结住了,不管你要修改还是要删除都不生效了,一旦访问 data.name 都一律返回 雷猴


以上就是 Object.defineProperty 的基础用法。






深度监听


上面的例子是监听基础的对象。但如果对象里还包含对象,这种情况就可以使用递归的方式。


递归需要创建一个方法,然后判断是否需要重复调用自身。


// 触发更新视图
function updateView() {
console.log('视图更新')
}

// 重新定义属性,监听起来(核心)
function defineReactive(target, key, value) {

// 深度监听
observer(value)

// 核心 API
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
if (newValue != value) {
// 深度监听
observer(newValue)

// 设置新值
// 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
value = newValue

// 触发视图更新
updateView()
}
}
})
}

// 深度监听
function observer(target) {
if (typeof target !== 'object' || target === null) {
// 不是对象或数组
return target
}

// 重新定义各个属性(for in 也可以遍历数组)
for (let key in target) {
defineReactive(target, key, target[key])
}
}

// 准备数据
const data = {
name: '雷猴'
}

// 开始监听
observer(data)

// 测试1
data.name = {
lastName: '鲨鱼辣椒'
}

// 测试2
data.name.lastName = '蟑螂恶霸'

上面这个例子会输出2次“视图更新”。




我创建了一个 updateView 方法,该方法模拟更新 DOM (类似 Vue的操作),但我这里简化成只是输出 “视图更新” 。因为这不是本文的重点。




测试1 会触发一次 “视图更新” ;测试2 也会触发一次。


因为在 Object.definePropertyset 里面我有调用了一次 observer(newValue)observer 会判断传入的值是不是对象,如果是对象就再次调用 defineReactive 方法。


这样可以模拟一个递归的状态。




以上就是 深度监听 的原理,其实就是递归。


但递归有个不好的地方,就是如果对象层次很深,需要计算的量就很大,因为需要一次计算到底。






监听数组


数组没有 key ,只有 下标。所以如果需要监听数组的内容变化,就需要将数组转换成对象,并且还要模拟数组的方法。


大概的思路和编码流程顺序如下:


  1. 判断要监听的数据是否为数组
  2. 是数组的情况,就将数组模拟成一个对象
  3. 将数组的方法名绑定到新创建的对象中
  4. 将对应数组原型的方法赋给自定义方法



代码如下所示


// 触发更新视图
function updateView() {
console.log('视图更新')
}

// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原形指向 oldArrayProperty,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);

['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName = 收起阅读 »

JS 将伪数组转换成数组

在 JS 中,伪数组 是非常常见的,它也叫 类数组。伪数组可能会给 JS 初学者带来一点困扰。 本文将详细讲解 什么是伪数组,以及分别在 ES5 和 ES6 中将伪数组转换成真正的数组 。 什么是伪数组? 伪数组的主要特征:它是一个对象,并且该对象有 le...
继续阅读 »




JS 中,伪数组 是非常常见的,它也叫 类数组。伪数组可能会给 JS 初学者带来一点困扰。


本文将详细讲解 什么是伪数组,以及分别在 ES5ES6 中将伪数组转换成真正的数组






什么是伪数组?


伪数组的主要特征:它是一个对象,并且该对象有 length 属性


比如


let arrayLike = {
"0": "a",
"1": "b",
"2": "c",
"length": 3
}

像上面的 arrayLike 对象,有 length 属性,key 也是有序序列。可以遍历,也可以查询长度。但却不能调用数组的方法。比如 push、pop 等方法。


ES6 之前,还有一个常见的伪数组:arguments


arguments 看上去也很像一个数组,但它没有数组的方法。


比如 arguments.push(1) ,这样做一定会报错。




除了 arguments 之外,NodeList 对象表示节点的集合也是伪数组,比如通过 document.querySelectorAll 获取的节点集合等。






转换


将伪数组转换成真正的数组的方法不止一个,我们先从 ES5 讲起。




ES5 的做法


在 ES6 问世之前,开发者通常需要用以下的方法把伪数组转换成数组。




方法1


// 通过 makeArray 方法,把数组转成伪数组
function makeArray(arrayLike) {
let result = [];
for (let i = 0, len = arrayLike.length; i < len; i++) {
result.push(arrayLike[i]);
}
return result;
}

function doSomething () {
let args = makeArray(arguments);
console.log(args);
}

doSomething(1, 2, 3);

// 输出: [1, 2, 3]

这个方法虽然有效,但要多写很多代码。




方法2


function doSomething () {
let args = Array.prototype.slice.call(arguments);
console.log(args);
}
doSomething(1, 2, 3);

// 输出: [1, 2, 3]

这个方法的功能和 方法1 是一样的,虽然代码量减少了,但不能很直观的让其他开发者觉得这是在转换。




ES6的做法


直到 ES6 提供了 Array.from 方法完美解决以上问题。


function doSomething () {
let args = Array.from(arguments);
console.log(args);
}

doSomething('一', '二', '三');

// 输出: ['一', '二', '三']

Array.from 的主要作用就是把伪数组和可遍历对象转换成数组的。




说“主要作用”的原因是因为 Array.from 还提供了2个参数可传。这样可以延伸很多种小玩法。


Array.from 的第二个参数是一个函数,类似 map遍历 方法。用来遍历的。


Array.from 的第三个参数接受一个 this 对象,用来改变 this 指向。




第三个参数的用法(不常用)


let helper = {
diff: 1,
add (value) {
return value + this.diff; // 注意这里有个 this
}
};

function translate () {
return Array.from(arguments, helper.add, helper);
}

let numbers = translate(1, 2, 3);

console.log(numbers); // 2, 3, 4



Array.from 其他玩法


创建长度为5的数组,且初始化数组每个元素都是1


let array = Array.from({length: 5}, () => 1)
console.log(array)

// 输出: [1, 1, 1, 1, 1]

第二个参数的作用和 map遍历 差不多的,所以 map遍历 有什么玩法,这里也可以做相同的功能。就不多赘述了。




把字符串转换成数组


let msg = 'hello';
let msgArr = Array.from(msg);
console.log(msgArr);

// 输出: ["h", "e", "l", "l", "o"]

如果传一个真正的数组给 Array.from 会返回一个一模一样的数组。

 
收起阅读 »

我写了一个将 excel 文件转化成 本地json文件的插件

插件介绍 excel-2b-json 插件用于将 google excel 文件转化成 本地json文件。 适用场景: 项目国际化,配置多语言 使用方法 1. 安装excel-2b-json npm install excel-2b-json 2. 引入使用...
继续阅读 »


插件介绍


excel-2b-json 插件用于将 google excel 文件转化成 本地json文件。


适用场景: 项目国际化,配置多语言


使用方法


1. 安装excel-2b-json


npm install excel-2b-json

2. 引入使用


const excelToJson = require('excel-2b-json');
// path 生成的json文件目录

excelToJson('https://docs.google.com/spreadsheets/d/12q3leiNxdmI_ZLWFj4LP_EA5PeJpLF18vViuyiSOuvM/edit#gid=0', path)


转化得到



下面是插件的实现



源码已放到github:github.com/Sunny-lucki…



一、涉及的算法


1. 26字母转换成数字,26进制,a为1,aa为27,ab为28


  function colToInt(col) {
const letters = ['', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
col = col.trim().split('')
let n = 0

for (let i = 0; i < col.length; i++) {
n *= 26
n += letters.indexOf(col[i])
}

return n
}

2. 生成几行几列的二维空数组


function getEmpty2DArr(rows, cols) {
let arrs = new Array(rows);
for (var i = 0; i < arrs.length; i++) {
arrs[i] = new Array(cols).fill(''); //每行有cols列
}
return arrs;
}


3. 清除二维数组中空的数组


[
[1,2,3],
['','',''],
[7,8,9]
]

转化为
[
[1,4,7],
[3,6,9]
]

  clearEmptyArrItem(matrix) {
return matrix.filter(function (val) {
return val.some(function (val1) {
return val1.replace(/\s/g, '') !== ''
})
})
}


4. 矩阵的翻转


[
[1,2,3],
[4,5,6],
[7,8,9]
]

转化为
[
[1,4,7],
[2,5,8],
[3,6,9]
]

算法实现


  /**
*
* @param {array*2} matrix 一个二维数组,返回旋转后的二维数组。
*/

rotateExcelDate(matrix) {
if (!matrix[0]) return []
var results = [],
result = [],
i,
j,
lens,
len
for (i = 0, lens = matrix[0].length; i < lens; i++) {
result = []
for (j = 0, len = matrix.length; j < len; j++) {
result[j] = matrix[j][i]
}
results.push(result)
}
return results
}

二、插件的实现


1. 下载google Excel文档到本地


我们先看看google Excel文档的url的组成


https://docs.google.com/spreadsheets/d/文档ID/edit#哈希值

例如下面这条,你可以尝试打开,下面这条链接是可以打开的。


https://docs.google.com/spreadsheets/d/12q3leiNxdmI_ZLWFj4LP_EA5PeJpLF18vViuyiSOuvM/edit#gid=0


下载google文档的步骤非常简单,只要获取原始的链接,然后拼接成下面的url,向这个Url发起请求,然后以流的方式写入生成文件就可以了。


https://docs.google.com/spreadsheets/d/ + "文档ID" + '/export?format=xlsx&id=' + id + '&' + hash

因此实现下载的方法非常简单,可以直接看代码


downLoadExcel.js



const fs = require('fs')
const request = require('superagent')
const rmobj = require('./remove')

/**
* 下载google excel 文档到本地
* @param {*} url // https://docs.google.com/spreadsheets/d/12q3leiNxdmI_ZLWFj4LP_EA5PeJpLF18vViuyiSOuvM/edit#gid=0
* @returns
*/

function downLoadExcel(url) {

// 记录当前下载文件的目录,方便删除
rmobj.push({
path: __dirname,
ext: 'xlsx'
})
return new Promise((resolve, reject) => {
var down1 = url.split('/')
var down2 = down1.pop() // edit#gid=0
var url2 = down1.join('/') // https://docs.google.com/spreadsheets/d/12q3leiNxdmI_ZLWFj4LP_EA5PeJpLF18vViuyiSOuvM
var id = down1.pop() // 12q3leiNxdmI_ZLWFj4LP_EA5PeJpLF18vViuyiSOuvM
var hash = down2.split('#').pop() // gid=0
var downurl = url2 + '/export?format=xlsx&id=' + id + '&' + hash // https://docs.google.com/spreadsheets/d/12q3leiNxdmI_ZLWFj4LP_EA5PeJpLF18vViuyiSOuvM/export?format=xlsx&id=12q3leiNxdmI_ZLWFj4LP_EA5PeJpLF18vViuyiSOuvM&gid=0
var loadedpath = __dirname + '/' + id + '.xlsx'
const stream = fs.createWriteStream(loadedpath)
const req = request.get(downurl)
req.pipe(stream).on('finish', function () {
resolve(loadedpath)
// 已经成功下载下来了,接下来将本地excel转化成json的工作就交给Excel对象来完成
})
})

}

module.exports = downLoadExcel

入口文件可以这样写


async function excelToJson(excelPathName, outputPath) {
if (Util.checkAddress(excelPathName) === 'google') {
// 1.判断是谷歌excel文档,需要交给Google对象去处理,主要是下载线上的,生成本地excel文件
const filePath = await downLoadExcel(excelPathName)

// 2.解析本地excel成二维数组
const data = await parseXlsx(filePath)

// 3.生成json文件
generateJsonFile(data, outputPath)
}

}
module.exports = excelToJson


之所以写if判断,是为了后面扩展,也许就不止是解析google文档了,或许也要解析腾讯等其他文档呢


第一步已经实现了,接下来就看第二步怎么实现


2. 解析本地excel成二维数组


解析本地excel文件,获取excel的sheet信息和strings信息


excel 文件其实本质上是多份xml文件的压缩文件。



xml是存储数据的,而html是显示数据的



而在这里我们只需要获取两份xml 文件,一份是strings,就是excel里的内容,一份是sheet,概括整个excel文件的信息。


async function parseXlsx(path) {

// 1. 解析本地excel文件,获取excel的sheet信息和content信息
const files = await extractFiles(path);

// 2. 根据strings和sheet解析成二维数组
const data = await extractData(files)

// 3. 处理二维数组的内容,
const fixData = handleData(data)
return fixData;
}

所以第一步我们看看怎么获取excel的sheet信息和strings信息


function extractFiles(path) {

// excel的本质是多份xml组成的压缩文件,这里我们只需要xl/sharedStrings.xml和xl/worksheets/sheet1.xml
const files = {
strings: {}, // strings内容
sheet: {},
'xl/sharedStrings.xml': 'strings',
'xl/worksheets/sheet1.xml': 'sheet'
}

const stream = path instanceof Stream ? path : fs.createReadStream(path)

return new Promise((resolve, reject) => {
const filePromises = [] // 由于一份excel文档,会被解析成好多分xml文档,但是我们只需要两份xml文档,分别是(xl/sharedStrings.xml和xl/worksheets/sheet1.xml),所以用数组接受

stream
.pipe(unzip.Parse())
.on('error', reject)
.on('close', () => {
Promise.all(filePromises).then(() => {
return resolve(files)
})
})
.on('entry', entry => {

// 每解析某个xml文件都会进来这里,但是我们只需要xl/sharedStrings.xml和xl/worksheets/sheet1.xml,并将内容保存在strings和sheet中
const file = files[entry.path]
if (file) {
let contents = ''
let chunks = []
let totalLength = 0
filePromises.push(
new Promise(resolve => {
entry
.on('data', chunk => {
chunks.push(chunk)
totalLength += chunk.length
})
.on('end', () => {
contents = Buffer.concat(chunks, totalLength).toString()
files[file].contents = contents
if (/�/g.test(contents)) {
throw TypeError('本次转化出现乱码�')
} else {
resolve()
}
})
})
)
} else {
entry.autodrain()
}
})
})
}

可以断点看看entry.path,你就会看到分别进来了好几次,然后我们会分别看到我们想要的那两个文件



两份xml文件解析之后就会到close方法里了,这时就可以看到strings和sheet都有内容了,而且内容都是xml



我们分别看看strings和sheet的内容


stream
.pipe(unzip.Parse())
.on('error', reject)
.on('close', () => {
Promise.all(filePromises).then(() => {
console.log(files.strings.contents);
console.log(files.sheet.contents);
return resolve(files)
})
})


格式化一下


strings



sheet


可以发现strings的内容非常简单,现在我们借助xmldom将内容解析为节点对象,然后用xpath插件来获取内容


xpath的用法:github.com/goto100/xpa…


  const XMLDOM = require('xmldom')
const xpath = require('xpath')
const ns = { a: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main' }
const select = xpath.useNamespaces(ns)

const valuesDoc = new XMLDOM.DOMParser().parseFromString(
files.strings.contents
)

// 把所有每个格子的内容都放进了values数组里。
values = select('//a:si', valuesDoc).map(string =>

select('.//a:t', string)
.map(t => t.textContent)
.join('')
)


'//a:si' 是xpath语法,//表示选择当前节点下的所有子孙节点,a是schemas.openxmlformats.org/spreadsheet…




可以看到,xpath的用法很简单,就是找到si节点下的子节点t的内容,然后放进数组里



最终生成的values数组是[ 'lang', 'cn','en', 'lang001','我是阳光', 'i am sunny','lang002', '前端阳光','FE Sunny', 'lang003','带带我', 'ddw']


现在我们要获取sheet的内容了,我们先分析一下xml结构



可以看到sheetData节点其实就是记录strings的内容的信息的,strings的内容是我们真正输入的,而sheet则是类似一种批注。


我们分析看看


row就是表示表格中的行,c则表示的是列,属性t="s"表示的是当前这个格子有内容,r="A1"表示的是在第一行中的A列



而节点v则表示该格子是该表格的第几个有值的格子,不信?我们可以试试看




可以看到这打印出来的xml内容,strings中已经没有了那两个值,而sheet中的那两个格子的c节点的t属性没了,而且v节点也没有了。


现在我们可以知道,string只保存有值的格子里的值,而sheet则是一个网格,不管格子有没有值都会记录,有值的会有个序号存在v节点中。


现在就要收集c节点


  const na = {
textContent: ''
}

class CellCoords {
constructor(cell) {
cell = cell.split(/([0-9]+)/)
this.row = parseInt(cell[1])
this.column = colToInt(cell[0])
}
}

class Cell {
constructor(cellNode) {
const r = cellNode.getAttribute('r')
const type = cellNode.getAttribute('t') || ''
const value = (select('a:v', cellNode, 1) || na).textContent
const coords = new CellCoords(r)

this.column = coords.column // 该格子所在列数
this.row = coords.row // 该格子所在行数
this.value = value // 该格子的顺序
this.type = type // 该格子是否为空
}
}

const cells = select('/a:worksheet/a:sheetData/a:row/a:c', sheet).map(
node => new Cell(node)
)

每个c节点用cell对象来表示


可以看到cell节点有四个属性。


你现在知道它为什么要保存顺序了吗?


因为这样才可以直接从strings生成的values数组中拿出对应顺序的值填充到网格中。


接下来要获取总共有多少列数和行数。这就需要获取最大最小行数列数,然后求差得到


// 计算该表格的最大最小列数行数
d = calculateDimensions(cells)

const cols = d[1].column - d[0].column + 1
const rows = d[1].row - d[0].row + 1

function calculateDimensions(cells) {
const comparator = (a, b) => a - b
const allRows = cells.map(cell => cell.row).sort(comparator)
const allCols = cells.map(cell => cell.column).sort(comparator)
const minRow = allRows[0]
const maxRow = allRows[allRows.length - 1]
const minCol = allCols[0]
const maxCol = allCols[allCols.length - 1]

return [{ row: minRow, column: minCol }, { row: maxRow, column: maxCol }]
}

接下来就根据列数和行数造空二维数组,然后再根据cells和values填充内容


  // 计算该表格的最大最小列数行数
d = calculateDimensions(cells)

const cols = d[1].column - d[0].column + 1
const rows = d[1].row - d[0].row + 1

// 生成二维空数组
data = getEmpty2DArr(rows, cols)

// 填充二维空数组
for (const cell of cells) {
let value = cell.value

// s表示该格子有内容
if (cell.type == 's') {
value = values[parseInt(value)]
}

// 填充该格子
if (data[cell.row - d[0].row]) {
data[cell.row - d[0].row][cell.column - d[0].column] = value
}
}
return data

我们看看最终生成的data,可以发现,excel的网格已经被二维数组模拟出来了



所以我们看看extractData的完整实现


function extractData(files) {
let sheet
let values
let data = []

try {
sheet = new XMLDOM.DOMParser().parseFromString(files.sheet.contents)
const valuesDoc = new XMLDOM.DOMParser().parseFromString(
files.strings.contents
)

// 把所有每个格子的内容都放进了values数组里。
values = select('//a:si', valuesDoc).map(string =>
select('.//a:t', string)
.map(t => t.textContent)
.join('')
)

console.log(values);
} catch (parseError) {
return []
}



const na = {
textContent: ''
}

class CellCoords {
constructor(cell) {
cell = cell.split(/([0-9]+)/)
this.row = parseInt(cell[1])
this.column = colToInt(cell[0])
}
}

class Cell {
constructor(cellNode) {
const r = cellNode.getAttribute('r')
const type = cellNode.getAttribute('t') || ''
const value = (select('a:v', cellNode, 1) || na).textContent
const coords = new CellCoords(r)

this.column = coords.column // 该格子所在列数
this.row = coords.row // 该格子所在行数
this.value = value // 该格子的顺序
this.type = type // 该格子是否为空
}
}

const cells = select('/a:worksheet/a:sheetData/a:row/a:c', sheet).map(
node => new Cell(node)
)

// 计算该表格的最大最小列数行数
d = calculateDimensions(cells)

const cols = d[1].column - d[0].column + 1
const rows = d[1].row - d[0].row + 1

// 生成二维空数组
data = getEmpty2DArr(rows, cols)

// 填充二维空数组
for (const cell of cells) {
let value = cell.value

// s表示该格子有内容
if (cell.type == 's') {
value = values[parseInt(value)]
}

// 填充该格子
if (data[cell.row - d[0].row]) {
data[cell.row - d[0].row][cell.column - d[0].column] = value
}
}
return data
}


接下来就是要去除空行和空列,并将二维数组翻转成我们需要的格式


function handleData(data) {
if (data) {
data = clearEmptyArrItem(data)
data = rotateExcelDate(data)
data = clearEmptyArrItem(data)
}
return data
}


可以看到,现在数组的第一项子数组则是key列表了。


接下来就可以根据key来生成对应的json文件了。


3. 生成json数据


这一步非常简单


function generateJsonFile(excelDatas, outputPath) {

// 获得转化成json格式
const jsons = convertProcess(excelDatas)

// 生成写入文件
writeFile(jsons, outputPath)
}

首先就是获取json数据


先获取data数组的第一项数组,第一项数组是key,然后生成每种语言的json对象


  /**
*
* @param {array*2} data
* 返回处理完后的多语言数组,每一项都是一个json对象。
*/

function convertProcess(data) {
var keys_arr = [],
data_arr = [],
result_arr = [],
i,
j,
data_arr_len,
col_data_json,
col_data_arr,
data_arr_col_len
// 表格合并处理,这是json属性列。
keys_arr = data[0]
// 第一例是json描述,后续是语言包
data_arr = data.slice(1)

for (i = 0, data_arr_len = data_arr.length; i < data_arr_len; i++) {
// 取出第一个列语言包
col_data_arr = data_arr[i]
// 该列对应的临时对象
col_data_json = {}
for (
j = 0, data_arr_col_len = col_data_arr.length;
j < data_arr_col_len;
j++
) {

col_data_json[keys_arr[j]] = col_data_arr[j]
}
result_arr.push(col_data_json)
}

return result_arr
}


我们可以看看生成的result_arr



可见已经成功生成每一种语言的json对象了。


接下来只需要生成json文件就可以了,注意把之前生成的excel文件删除


  //得到的数据写入文件
function writeFile(datas, outputPath) {
for (let i = 0, len = datas.length; i < len; i++) {
fs.writeFileSync(outputPath +
(datas[i].filename || datas[i].lang) +
'.json',
JSON.stringify(datas[i], null, 4)
)
}
rmobj.flush();
}

到此,一个稍微完美的插件就此完成了。 撒花撒花!!!!

收起阅读 »

V8系列第二篇:从执行上下文的角度看JavaScript到底是怎么运行的

1.前言 先来说一说V8引擎和浏览器 V8引擎要想运行起来,就必须依附于浏览器,或者依附于Node.js宿主环境。因此V8引擎是被浏览器或者Node.js启动的。比如在Chrome浏览器中,你打开一个网址后,渲染进程便会初始化V8引擎,同时在V8中会初始化堆空...
继续阅读 »


1.前言


先来说一说V8引擎和浏览器


V8引擎要想运行起来,就必须依附于浏览器,或者依附于Node.js宿主环境。因此V8引擎是被浏览器或者Node.js启动的。比如在Chrome浏览器中,你打开一个网址后,渲染进程便会初始化V8引擎,同时在V8中会初始化堆空间和栈空间,而栈空间就是来管理执行上下文的。而执行上下文就可以说是我们平常写的JavaScript代码的运行环境。


好了简单理解一下,那接下来本篇就重点来学习一下V8引擎中的执行上下文


2.执行上下文概述



首先从宏观的角度来说: JavaScript代码要想能够被执行,就必须先被V8引擎编译,编译完成之后才会进入到执行阶段,总结为六个字:先编译再执行



在V8引擎编译的过程中,同时会生成执行上下文。最开始执行代码的时候通常会生成全局执行上下文、执行一个函数时会生成该函数的执行上下文、当执行一个代码块时也会生成代码块的可执行上下文。所以一段代码可以说成是先编译再执行,那么整个过程就是无数个先编译再执行构成的(通常编译发生在执行代码前的几微秒,甚至更短的时间)。


我们再来理解一下上面说到的执行上下文, 在JavaScript 高级程序设计(第四版)中大概是这样描述的:



执行上下文的概念在JavaScript中是非常重要的。变量或者函数的执行上下文决定了它们可以访问哪些数据,以及他们拥有哪些行为(可以执行哪些方法吧)。每个执行上下文都有一个关联的变量对象,而这个执行上下文中定义的所有变量和函数都存在于这个对象上。这个对象我们在代码中是无法访问的。



执行上下文可以说是执行一段JavaScript代码的运行环境,可以算作是一个抽象的概念。


简单的理解一下概念(下文如果再需要的时候你可以返回顶部再次理解查看)后,我们就来看看JavaScript是怎么将一个变量和函数运行起来的。


3.准备测试代码


这里为了更直观的查看代码的运行效果,我特意新建了一个xxxx.html文件,文件所有代码如下所示:
这里突然发现html文件中只有script标签和js代码也是可以执行的,不清楚以前是不是也是可以,还是说JavaScript引擎在后期做了优化处理。


<script>
a_Function()
var a_variable = 'aehyok'
console.log(a_variable)
function a_Function() {
console.log('函数a_Function执行了', a_variable);
}
</script>


特别强调一个点,我上面声明变量使用的var关键字



运行后的执行结果


image.png


4.调试var声明的变量


相信通过运行结果,你心中应该有了自己的代码执行过程了。我们接着往下操作,在第2行代码(下文截图中的位置)打个调试断点,如下图所示


image.png


此时代码已经准备开始要执行a_Function函数了。脑补一下,我们就以此为分割点(按正常来说这肯定是不合理的,因为代码已经开始执行了,不过你可以暂且这样尝试去理解一下),就是运行到第2行代码之前的时间段或者状态,我们就称它为编译阶段,这之后代码就开始运行了,我们称它为执行阶段


1、通过截图可以发现,作用域下的全局 已经有了一个a_Function函数,以及一个a_variable变量其值为 undefined,这里可以看到许许多多的其他变量、函数,这其实就是全局window对象。


2、使用过JavaScript的人都清楚,JavaScript是按照顺序执行代码的,但是通过截图去看,好像又不太对劲,所以执行前的编译阶段,JavaScript引擎还是处理了不少事情的,它做了什么事情呢?


V8引擎编译这段代码的时候,同时会生成一个全局执行上下文,在截图的第二行代码发现是一个函数,便会在代码中查找到该函数的定义,并将该函数体放到全局执行上下文词法环境中。该函数体里的代码还未执行,所以不会去编译,继续第三行代码,发现是var声明的一个变量,便会将该变量放到全局执行上文变量环境中,同时给该变量赋值为undefined。


具体如下模拟代码


function a_Function() {
console.log('函数a_Function执行了', a_variable);
}
var a_variable = undefined

这段代码主要在编译代码阶段做了变量提升,会将var声明的变量存放到变量环境中(let和const声明的变量存放到词法环境中),而函数的声明会被存放到词法环境中。
词法环境变量环境是存在于执行上下文的,变量的默认值会被设置为undefined,函数的执行体会被带到词法环境
然后还会生成可执行代码,其实编译生成的是字节码,下面的代码算是模拟代码:


a_Function()
a_variable = 'aehyok'
console.log(a_variable)

  • 执行阶段
    接下来开始按照顺序执行上面生成的可执行代码,其实在执行阶段已经变成了机器码

a_Function()
a_variable = 'aehyok'
console.log(a_variable)

第一行模拟代码:先调用a_Function,此时会开始生成该函数的函数执行上下文, 执行a_Function中的代码,函数a_Function执行了 undefined,因为此时的a_variable还没给予赋值操作


第二行模拟代码:对a_variable变量进行赋值字符串"aehyok",此时变量环境中的a_variable值变为"aehyok"


第三行模拟代码:打印已经赋值为aehyok的变量。


5.调试let声明的变量


5.1主要是将上面的测试代码中:声明变量的关键字var改为let


<script>
a_Function()
let a_variable = 'aehyok'
console.log(a_variable)
function a_Function() {
console.log('函数a_Function执行了', a_variable);
}
</script>

执行代码以后发现直接报错了,报错内容如下图所示


image.png


5.2打断点调试代码


image.png
代码断点打到如截图中第2行位置,可以看到let声明的变量,存在于单独的Script作用域中,并且赋值为undefined。


5.3分析5.1和5.2的代码


  • 通过varlet两种方式代码运行比对情况来看,let声明变量的方式不存在变量提升的情况。
  • 通过3.2截图可以发现,let声明变量的方式,在作用域中的已经创建,并赋值为undefined,但通过查阅资料发现:


let声明的变量,主要是因为V8虚拟机做了限制,虽然a_variable已经在内存中并且赋值为undefined,但是当你在let a_variable 之前访问a_variable时,根据ECMAScript定义,虚拟机会阻止的访问!也可以说成是形成了暂时性的死区,这是语法规定出来的。所以就会报错。



6.调试let声明的变量继续执行


主要添加了一个let声明的变量,以及为其进行了赋值操作,代码如下所示


<script>
a_Function()
var a_variable = 'a_aehyok'
let aa_variable = 'aa_aehyok'
console.log(a_variable)
function a_Function() {
console.log('函数a_Function执行了', a_variable);
}
</script>

执行后情况截图如下


image.png


可以发现通过var声明的变量和let(也可以使用const)声明的变量被储存在了不同的位置,之前上面说过通过var声明的变量被存放到了变量环境中了。那么现在我再告诉你,通过let(也可以是const)声明的变量被存放到了词法环境中了。


  • var声明的变量存放在变量环境
  • let和const声明的变量存放在词法环境
  • 函数的声明存放在词法环境
  • 变量环境词法环境都存在于执行上下文

7.总结三种执行上下文


在上面的一小段代码中,我们已经使用过了两种执行上下文,全局执行上下文函数执行上下文


  • 全局执行上下文 — 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。


var声明的变量会在全局window对象上,而let和const声明的变量是不会在全局window对象上的。而全局函数时会在全局window对象上。




  • 函数执行上下文 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。



  • Eval 函数执行上下文 — 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用 eval,所以在这里我不会讨论它。



8.总结



  • 1、通过这篇简单的文章,我想我自己理清楚了,原来JavaScript代码是先编译再执行的。



  • 2、然后代码在编译的时候就生成了执行上下文,也就是代码运行的环境。



  • 3、var声明的变量存在变量提升,并且在编译阶段存放到了变量环境中,变量环境其实也是一个词法环境



  • 4、通过变量提升发现,代码会先生成执行上下文,然后再生成可执行的代码



  • 5、const和let声明的变量不存在变量提升,并且再编译阶段被存放到了词法环境中。



  • 6、所有var定义的全局变量和全局定义的函数,都会在window对象上。



  • 7、所有let和const定义的全局变量不会定义在全局上下文中,但是在作用域链的解析效果上是一样的(跟var定义的)。

 
收起阅读 »

V8开篇:V8是如何运行JavaScript(let a = 1)代码的?

我们知道,机器是不能直接理解我们平常工作或者自己学习的代码的。所以,在执行程序之前,需要将代码翻译成机器能读懂的机器语言。按语言的执行流程,可以把计算机语言划分为编译型语言和解释型语言: 编译型语言:在代码运行前编译器直接将对应的代码转换成机器码,运行时不需...
继续阅读 »


我们知道,机器是不能直接理解我们平常工作或者自己学习的代码的。所以,在执行程序之前,需要将代码翻译成机器能读懂的机器语言。按语言的执行流程,可以把计算机语言划分为编译型语言和解释型语言:



编译型语言:在代码运行前编译器直接将对应的代码转换成机器码,运行时不需要再重新翻译,直接可以使用编译后的结果。




解释型语言:需要将代码转换成机器码,和编译型语言的区别在于运行时需要转换。解释型语言的执行速度要慢于编译型语言,因为解释型语言每次执行都需要把源码转换一次才能执行。



Java 和 C++ 等语言都是编译型语言,而 JavaScript 是解释性语言,它整体的执行速度会略慢于编译型的语言。V8 是众多JavaScript引擎中性能表现最好的一个,并且它是 Chrome 的内核,Node.js 也是基于 V8 引擎研发的。


1.运行的整体过程


未命名文件 (4).png


2.英译汉翻译的过程


比如我们看到了google V8官网的一篇英文文章 v8.dev/blog/faster…,在阅读的过程中,可以就是要对每一个单词进行解析翻译成中文,然后多个单词进行语法的解析,再通过对整句话进行整个语句进行解析,那么这句话就翻译结束了。


下面我们就举例一句英文的翻译过程:I am a programmer。


  • 1、首先对输入的字符串I am a programmer。进行拆分便会拆分成 I am a programmer


相当于词法分析




  • 2、I 是一个主语, am 是一个谓语, a是一个形容词, programmer是个名词, 标点符号。



  • 3、I的意思, am的意思, a一个的意思, programmer程序员的意思, 句号的意思。




2和3一起相当于语法分析



  • 4、对3中的语法分析进行拼接处理:我是一个程序员。当然这是非常简单的一个英译汉,一篇文章的话,就会复杂一些了。


相当于语义分析



3.V8运行的整个过程


3.1.准备一段JavaScript源代码


let a = 10

3.2.词法分析:


一段源代码,就是一段字符串。编译器识别源代码的第一步就是要进行分词,将源代码拆解成一个个的token。所谓的token,就是不可再分的单个字符或者字符串。


3.3.token


通过 esprima.org/demo/parse.… 可以查看生成的tokens,也就是上面那段源代码生成的所有token。


Token类别: 关键字、标识符、字面量、操作符、数据类型(String、Numeric)等


image.png


3.4.语法分析


将上一步生成的 token 数据,根据语法规则转为 AST。通过astexplorer.net 可以查看生成AST抽象语法树。


3.5.AST


生成的AST如下图所示,生成过程就是先分词(词法分析),再解析(语法分析)


image.png
当然你也可以查看生成的AST的JSON结构


{
"type": "Program",
"start": 0,
"end": 9,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 9,
"declarations": [
{
"type": "VariableDeclarator",
"start": 4,
"end": 9,
"id": {
"type": "Identifier",
"start": 4,
"end": 5,
"name": "a"
},
"init": {
"type": "Literal",
"start": 8,
"end": 9,
"value": 1,
"raw": "1"
}
}
],
"kind": "let"
}
],
"sourceType": "module"
}

同样我在本地下载了v8,直接用v8来查看AST


v8-debug  --print-ast hello.js

image.png


3.6.解释器


解释器会将AST生成字节码,生成字节码的过程也就是对AST抽象语法树进行遍历循环,并进行语义分析


3.7.字节码


在最开始的V8引擎中是没有字节码,是直接将AST转换生成为机器码。这种架构存在的问题就是内存消耗特别大,尤其是在移动设备上,编译出来的机器码占了整个chorme浏览器的三分之一,这样为代码运行时留下的内存就更小了。
于是后来在V8中加入了Ignition 解释器,引入字节码,主要就是为了减少内存消耗。
本地可以使用V8命令行查看生成的字节码


v8-debug  --print-bytecode hello.js

image.png


3.8.热点代码


首先判断字节码是否为热点代码。通常第一次执行的字节码,Ignition 解释器会逐条解释执行。在执行的过程中,如果发现是热点代码,比如for 循环中的代码被执行了多次,这种就称之为热点代码。那么后台的TurboFan就会把该段热点代码编译为高效的机器码,然后再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了, 这样就大大提升了代码的执行效率。


3.9.编译器


TurboFan编译器也可以说是JIT的即时编译器,也可以说是优化编译器。



Ignition 解释器: 可以将AST生成字节码,还可以解释执行字节码。



4、总结


  • 了解V8整个的运行机制
  • 学习JavaScript到底是怎么运行的
  • 对日后编写JavaScript代码有非常多的好处
  • 看完学习了,能提升我们的技术水平
  • 对于日后遇到问题,能够从底层去思考问题出在那里,更快速的定位和解决问题
  • 真的非常熟悉了,可以自己开发一门新的语言
 
收起阅读 »

一盏茶的功夫,拿捏作用域&作用域链

前言 我们需要先知道的是引擎,引擎的工作简单粗暴,就是负责javascript从头到尾代码的执行。引擎的一个好朋友是编译器,主要负责代码的分析和编译等;引擎的另一个好朋友就是今天的主角--作用域。那么作用域用来干什么呢?作用域链跟作用域又有什么关系呢? 一、作...
继续阅读 »


前言


我们需要先知道的是引擎,引擎的工作简单粗暴,就是负责javascript从头到尾代码的执行。引擎的一个好朋友是编译器,主要负责代码的分析和编译等;引擎的另一个好朋友就是今天的主角--作用域。那么作用域用来干什么呢?作用域链跟作用域又有什么关系呢?


一、作用域(scope)


作用域的定义:作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。


1、作用域的分类

  1. 全局作用域

var name="global";
function foo(){
console.log(name);
}
foo();//global

这里函数foo()内部并没有声明name变量,但是依然打印了name的值,说明函数内部可以访问到全局作用域,读取name变量。再来一个例子:


hobby='music';
function foo(){
hobby='book';
console.log(hobby);
}
foo();//book

这里全局作用域和函数foo()内部都没有声明hobby这个变量,为什么不会报错呢?这是因为hobby='music';写在了全局作用域,就算没有var,let,const的声明,也会被挂在window对象上,所以函数foo()不仅可以读取,还可以修改值。也就是说hobby='music';等价于window.hobby='music';


  1. 函数体作用域

函数体的作用域是通过隐藏内部实现的。换句话说,就是我们常说的,内层作用域可以访问外层作用域,但是外层作用域不能访问内层。原因,说到作用域链的时候就迎刃而解了。


function foo(){
var age=19;
console.log(age);
}
console.log(age);//ReferenceError:age is not defined

很明显,全局作用域下并没有age变量,但是函数foo()内部有,但是外部访问不到,自然而然就会报错了,而函数foo()没有调用,也就不会执行。


  1. 块级作用域

块级作用域更是见怪不怪,像我们接触的let作用域,代码块{},for循环用let时的作用域,if,while,switch等等。然而,更深刻理解块级作用域的前提是,我们需要先认识认识这几个名词:


--标识符:能在作用域生效的变量。函数的参数,变量,函数名。需要格外注意的是:函数体内部的标识符外部访问不到


--函数声明:function 函数名(){}


--函数表达式: var 函数名=function(){}


--自执行函数: (function 函数名(){})();自执行函数前面的语句必须有分号,通常用于隐藏作用域。


接下来我们就用一个例子,一口气展示完吧


function foo(sex){
console.log(sex);
}
var f=function(){
console.log('hello');
}
var height=180;
(
function fn(){
console.log(height);
}
)();
foo('female');
//依次打印:
//180
//female
//hello

分析一下:标识符:foo,sex,height,fn;函数声明:function foo(sex){};函数表达式:var f=function(){};自执行函数:(function fn(){})();需要注意,自执行函数fn()前面的var height=180;语句,分号不能抛弃。否则,你可以试一下。


二、预编译


说好只是作用域和作用域链的,但是考虑到理解作用域链的必要性,这里还是先聊聊预编译吧。先讨论预编译在不同环境发生的情况下,是如何进行预编译的。


  1. 发生在代码执行之前

(1)声明提升


console.log(b);
var b=123;//undefined

这里打印undefined,这不是报错,与Refference:b is not defined不同。这是代码执行之前,预编译的结果,等同于以下代码:


var b;//声明提升
console.log(b);//undefined
b=123;

(2)函数声明整体提升


test();//hello123  调用函数前并没有声明,但是任然打印,是因为函数声明整体提升了
function test(){
var a=123;
console.log('hello'+a);
}

2.发生在函数执行之前


理解这个只需要掌握四部曲


(1)创建一个AO(Activation Object)


(2)找形参和变量声明,然后将形参和变量声明作为AO的属性名,属性值为undefined


(3)将实参和形参统一


(4)在函数体内找函数声明,将函数名作为AO对象的属性名,属性值予函数体
那么接下来就放大招了:


var global='window';
function foo(name,sex){
console.log(name);
function name(){};
console.log(name);
var nums=123;
function nums(){};
console.log(nums);
var fn=function(){};
console.log(fn);
}
foo('html');

这里的结果是什么呢?分析如下:


//从上到下
//1、创建一个AO(Activation Object)
AO:{
//2、找形参和变量声明,然后将形参和变量声明作为AO的属性名,属性值为undefined
name:undefined,
sex:undefined,
nums=undefined,
fn:undefined,
//3、将实参和形参统一
name:html,
sex:undefined,
nums=123,
fn:function(){},
//4、在函数体内找函数声明,将函数名作为AO对象的属性名,属性值予函数体
name:function(){},
sex:undefined,
fn:function(){},
nums:123//这里不仅存在nums变量声明,也存在nums函数声明,但是取前者的值

以上步骤得到的值,会按照后面步骤得到的值覆盖前面步骤得到的值
}
//依次打印
//[Function: name]
//[Function: name]
//123
//[Function: fn]

3.发生在全局(内层作用域可以访问外层作用域)


同发生在函数执行前一样,发生在全局的预编译也有自己的三部曲:


(1)创建GO(Global Object)对象
(2)找全局变量声明,将变量声明作为GO的属性名,属性值为undefined
(3)在全局找函数声明,将函数名作为GO对象的属性名,属性值赋予函数体
举个栗子:


var global='window';
function foo(a){
console.log(a);
console.log(global);
var b;
}
var fn=function(){};
console.log(fn);
foo(123);
console.log(b);

这个例子比较简单,一样的步骤和思路,就不在赘述分析了,相信你已经会了。打印结果依次是:


[Function: fn]
123
window
ReferenceError: b is not defined

好啦,进入正轨,我们接着说作用域链。


三、作用域链


作用域链就可以帮我们找到,为什么内层可以访问到外层,而外层访问不到内层?但是同样的,在认识作用域链之前,我们需要见识见识一些更加晦涩抽象的名词。


  1. 执行期上下文:当函数执行的时候,会创建一个称为执行期上下文的对象(AO对象),一个执行期上下文定义了一个函数执行时的环境。 函数每次执行时,对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行期上下文,当函数执行完毕,它所产生的执行期上下文会被销毁。
  2. 查找变量:从作用域链的顶端依次往下查找。

3. [[scope]]:作用域属性,也称为隐式属性,仅支持引擎自己访问。函数作用域,是不可访问的,其中存储了运行期上下文的结合。


我们先看一眼函数的自带属性:


function test(){//函数被创建的那一刻,就携带name,prototype属性
console.log(123);
}
console.log(test.name);//test
console.log(test.prototype);//{} 原型
// console.log(test[[scope]]);访问不到,作用域属性,也称为隐式属性

// test() --->AO:{}执行完毕会回收
// test() --->AO:{}执行完毕会回收

接下来看看作用域链怎么实现的:


var global='window';
function foo(){
function fn(){
var fn=222;
}
var foo=111;
console.log(foo);
}
foo();

分析:


GO:{
foo:function(){}
}
fooAO:{
foo:111,
fn:function(){}
}
fnAO:{
fn:222
}
// foo定义时 foo.[[scope]]---->0:GO{}
// foo执行时 foo.[[scope]]---->0:AO{} 1:GO{} 后访问的在前面
//fn定义时 fn.[[scope]]---->0:fnAO{} 1:fooAO{} 2:GO{}
fnAO:fn的AO对象;fooAO:foo的AO对象

 


综上而言:作用域链就是[[scope]]中所存储的执行期上下文对象的集合,这个集合呈链式链接,我们把这种链式链接叫做作用域链。



收起阅读 »

Dart(三)—方法定义、箭头函数、函数相互调用、匿名、自执行方法及闭包

方法定义 dart自定义方法的基本格式: 返回类型 方法名称(参数1,参数2,...){ 方法体 return 返回值 / 或无返回值; } 定义方法的的几个例子: void printInfo(){ print('我是一个自定义方法');...
继续阅读 »

方法定义



dart自定义方法的基本格式:

返回类型 方法名称(参数1,参数2,...){
方法体
return 返回值 / 或无返回值;
}

定义方法的的几个例子:


void printInfo(){
print('我是一个自定义方法');
}

int getNum(){
var count = 123;
return count;
}

String printUserInfo(){

return 'this is str';
}

List getList(){

return ['111','2222','333'];
}

Dart没有public 、private等关键字,_ 下横向直接代表 private


方法的作用域


void main(){

void outFun(){
innerFun(){

print('aaa');
}
innerFun();
}

// innerFun(); 错误写法

outFun(); //调用方法
}

方法传参


一般定义:


String getUserInfo(String username, int age) {
//形参
return "姓名:$username -> 年龄:$age";
}

print(printUserInfo('小明', 23)); //实参

Dart中可以定义一个带可选参数的方法 ,可选参数需要指定类型默认值:


void main() {
String printUserInfo(String username, [int age = 0]) { //age格式表示可选
//形参
if (age != 0) {
return "姓名:$username -> 年龄:$age";
}
return "姓名:$username -> 年龄不详";
}

print(printUserInfo('小明', 28)); //实参
//可选就可以不传了
print(printUserInfo('李四'));
}

定义一个带默认参数的方法:


String getUserInfo(String username,[String sex='男',int age=0]){  //形参
if(age!=0){
return "姓名:$username -> 性别:$sex -> 年龄:$age";
}
return "姓名:$username -> 性别:$sex -> 年龄不详";
}
print(getUserInfo('张三'));
print(getUserInfo('李四','男'));
print(getUserInfo('李梅梅','女',25));

定义一个命名参数的方法,定义命名参数需要指定类型默认值:


命名参数的好处是在使用时可以不用按顺序赋值,看下面代码:


String getUserInfo(String username, {int age = 0, String sex = '男'}) {//形参
if (age != 0) {
return "姓名:$username -> 性别:$sex -> 年龄:$age";
}
return "姓名:$username -> 性别:$sex -> 年龄保密";
}
print(getUserInfo('张三',sex: '男',age: 20));

定义一个把方法当做参数的方法:


其实就是方法可以当做参数来用,这点和Kotlin也是一样的:


//方法1 随便打印一下
fun1() {
print('fun1');
}

//方法2 参数是一个方法
fun2(fun) {
fun();
}

//调用fun2这个方法 把fun1这个方法当做参数传入
fun2(fun1());

箭头函数和函数的相互调用


箭头函数


在之前的学习中,我们知道可以使用forEach来遍历List,其一般格式如下:


List list = ['a', 'b', 'c'];
list.forEach((value) {
print(value);
});

而箭头函数就是可以简写这种格式:


list.forEach((value) => print(value));

箭头后面指向的就是方法的返回值,这里要注意的是:



箭头函数内只能写一条语句,并且语句后面没有分号(;)



对于之前map转换的例子也可以使用箭头方法来简化一下:


List list = [1, 3, 6, 8, 9];
var newList = list.map((value) {
if (value > 3) {
return value * 2;
}
return value;
});

这里就是修改List里面的数据,让数组中大于3的值乘以2。那用箭头函数简化后可以写成:


var newList = list.map((value) => value > 3 ? value*2 : value);

一句代码完成,非常有意思。


函数的相互调用


  // 定义一个方法来判断一个数是否是偶数  
bool isEvenNumber(int n) {
if (n % 2 == 0) {
return true;
}
return false;
}
// 定义一个方法打印1-n以内的所有偶数
prinEvenNumber(int n) {
for (var i = 1; i <= n; i++) {
if (isEvenNumber(i)) {
print(i);
}
}
}
prinEvenNumber(10);

匿名方法、自执行方法及方法的递归


匿名方法


var printNum = (){
print(12);
};
printNum();

这里很明显跟Kotlin中的特性基本是一样的。带参数的匿名方法:


var printNum = (int n) {
print(n + 2);
};

printNum(3);

自执行方法


自执行方法顾名思义就是不需要调用,会自动去执行的,这是因为自执行函数的定义和调用合为了一体。当我们创建了一个匿名函数,并执行了它,由于外部无法引用的它的内部变量,所以在执行完就会很快被释放,而且这种做法不会污染到全局对象。看如下代码:


((int n) {
print("这是一个自执行方法 + $n");
})(666);
}

方法的递归


方法的递归无非就是在条件满足的条件下继续在方法内调用自己本身,看以下代码:


var sum = 0;
void fn(int n) {
sum += n;
if (n == 0) {
return;
}
fn(n - 1);
}
fn(100);
print(sum);

实现的是1加到100。


闭包


闭包是一个前端的概念,客户端开发早期使用Java可以说是不支持闭包,或是不完整的闭包,但Kotlin是可以支持闭包的操作。


闭包的意思就是函数嵌套函数, 内部函数会调用外部函数的变量或参数, 变量或参数不会被系统回收(不会释放内存)。所以闭包解决的两个问题是:



  • 变量常驻内存

  • 变量不污染全局


闭包的一般写法是:



  • 函数嵌套函数,并return 里面的函数,这样就形成了闭包


闭包的写法:


Function func() {
var a = 1; /*不会污染全局 常驻内存*/
return () {
a++;
print(a);
};
}

这里return匿名方法后,a的值就可以常驻内存了:


var mFun = func();
mFun();
mFun();
mFun();

打印:2、3、4。


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

Dart(二)—循环表达式、List、Set及Map的常用属性和方法

Dart的循环表达式 for循环 for (int i = 1; i<=100; i++) { print(i); } 也可以写成: for (var i = 1; i<=10; i++) { print(i); } 对于List...
继续阅读 »

Dart的循环表达式


for循环


for (int i = 1; i<=100; i++) {   
print(i);
}

也可以写成:


for (var i = 1; i<=10; i++) {
print(i);
}

对于List的遍历我们可以这样做:


var list = <String>["张三","李四","王五"];
for (var element in list) {
print(element);
}

对于Map的迭代我们也可以使用for循环语句:


var person={
"name":"小明",
"age":28,
"work":["程序员","Android开发"]
};

person.forEach((key, value) {
print(value);
});

while语句


while有两种语句格式:


while(表达式/循环条件){    

}

do{
语句/循环体

}while(表达式/循环条件);


注意:



  • 1、最后的分号不要忘记

  • 2、循环条件中使用的变量需要经过初始化

  • 3、循环体中,应有结束循环的条件,否则会造成死循环。



看下面代码:


int i = 1;
while (i <= 10) {
print(i);
i++;
}

do...while()最大的区别就是不管条件成立与否都会至少执行一次:


var i = 2;
do{
print('执行代码');
}while(i < 2);

break和continue语句


break语句功能:



  • switch语句中使流程跳出switch结构。

  • 在循环语句中使流程跳出当前循环,遇到break循环终止,后面代码也不会执行


需要强调的是:



  • 如果在循环中已经执行了break语句,就不会执行循环体中位于break后的语句。

  • 在多层循环中,一个break语句只能向外跳出一层


break可以用在switch case中 也可以用在for循环和while循环中。


continue语句的功能:


只能在循环语句中使用,使本次循环结束,即跳过循环体中下面尚未执行的语句,接着进行下次的是否执行循环的判断。


continue可以用在for循环以及while循环中,但是不建议用在while循环中,不小心容易死循环。


break使用:


//如果 i等于4的话跳出循环
for(var i=1;i<=10;i++){
if(i==4){
break; /*跳出循环体*/
}
print(i);
}

//break语句只能向外跳出一层
for(var i = 0;i < 5;i++){
for(var j = 0;j< 3;j++){
if(j == 1){
break;
}
}
}

while循环跳出:


//while循环 break跳出循环

var i = 1;

while(i< =10){
if(i == 4){
break;
}
print(i);
i++;
}

continue使用:


//如果i等于4的话跳过

for(var i=1;i<=5;i++){
if(i == 2){
continue; //跳过当前循环体 然后循环还会继续执行
}
print(i);
}

List常用属性和方法


常用属性:



  • length 长度

  • reversed 翻转

  • isEmpty 是否为空

  • isNotEmpty 是否不为空


常用方法:



  • add 增加

  • addAll 拼接数组

  • indexOf 查找 传入具体值

  • remove 删除 传入具体值

  • removeAt 删除 传入索引值

  • fillRange 修改

  • insert(index,value) 指定位置插入

  • insertAll(index,list) 指定位置插入List

  • toList() 其他类型转换成List

  • join() List转换成字符串

  • split() 字符串转化成List

  • forEach

  • map

  • where

  • any


一些常用属性和方法使用举例:


var list=['张三','李四','王五',"小明"];
print(list.length);
print(list.isEmpty);
print(list.isNotEmpty);
print(list.reversed); //对列表倒序排序

print(list.indexOf('李四')); //indexOf查找数据 查找不到返回-1 查找到返回索引值

list.remove('王五');

list.removeAt(2);

list.fillRange(1, 2,'a'); //修改 1是开始的位置 2二是结束的位置

print(list);

list.insert(1,'a');

print(list);

list.insertAll(1, ['a','b']); //插入多个

Set


Set的最主要的功能就是去除数组重复内容,它是没有顺序且不能重复的集合,所以不能通过索引去获取值。


var s = new Set();
s.add('A');
s.add('B');
s.add('B');

print(s); //{A, B}

add相同内容时候无法添加进去的。


Set可以通过add方法添加一个List,并清除值相同的元素:


var list = ['香蕉','苹果','西瓜','香蕉','苹果','香蕉','苹果'];
var s = new Set();
s.addAll(list);
print(s);
print(s.toList());

Map常用属性和方法


Map是无序的键值对,它的常用属性主要有以下:


常用属性:



  • keys 获取所有的key值

  • values 获取所有的value值

  • isEmpty 是否为空

  • isNotEmpty 是否不为空


常用方法:



  • remove(key) 删除指定key的数据

  • addAll({...}) 合并映射 给映射内增加属性

  • containsValue 查看映射内的值 返回true/false

  • forEach

  • map

  • where

  • any

  • every


map转换:


List list = [1, 3, 4];
//map转换,根据返回值返回新的元素列表
var newList = list.map((value) {
return value * 2;
});
print(newList.toList());

where:获取符合条件的元素:


List list = [1,3,4,5,7,8,9];

var newList = list.where((value){
return value > 5;
});
print(newList.toList());

any:是否有符合条件的元素


List list = [1, 3, 4, 5, 7, 8, 9];
//只要集合里面有满足条件的就返回true
var isContain = list.any((value) {
return value > 5;
});
print(isContain);

every:需要每一个都满足条件


List myList=[1,3,4,5,7,8,9];
//每一个都满足条件返回true 否则返回false
var flag = myList.every((value){

return value > 5;
});
print(flag);

Set使用forEach遍历:


var s=new Set();

s.addAll([11,22,33]);

s.forEach((value) => print(value));

Map使用forEach遍历:


Map person={
"name":"张三",
"age":28
};

person.forEach((key,value){
print("$key -> $value");
});

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

Dart(一)—变量、常量、基本类型、运算符、条件判断以及类型转换

前言 Dart语言跟Kotlin都是一种强大的脚本语言,它的很多语法跟Kotlin是很相似的。比如Dart也是可以不预先定义变量类型 ,自动会类型推倒,它修饰一般变量的关键字也是var,所以如果我们熟悉Kotlin,Dart也会很容易上手。 Dart变量和常量...
继续阅读 »

前言


Dart语言跟Kotlin都是一种强大的脚本语言,它的很多语法跟Kotlin是很相似的。比如Dart也是可以不预先定义变量类型 ,自动会类型推倒,它修饰一般变量的关键字也是var,所以如果我们熟悉KotlinDart也会很容易上手。


Dart变量和常量


变量


如前言所说,DartKotlin一样是强大的脚本类语言,可以不预先定义变量类型 ,自动会类型推倒,Dart中定义变量可以通过var关键字可以通过类型来申明变量:


var str = 'dart';

String str2 = 'this is dart';

int count = 123;


注意: var 后就不要写类型 , 写了类型 不要var 两者都写 var a int = 5; 报错



常量:final 和 const修饰符



  • const修饰的值不变 要在定义变量的时候就得赋值;

  • final可以开始不赋值 只能赋一次,而final不仅有const的编译时常量的特性,最重要的它是运行时常量,并且final是惰性初始化,即在运行时第一次使用前才初始化。


final name = 'Max';
final String sex = '男';

const bar = 1000000;
const double atm = 1.01325 * bar;

如果我们使用了阿里的代码规范插件,其实他会提示我们最好用const代替final


Dart的命名规则



  • 变量名称必须由数字、字母、下划线和美元符($)组成。

  • 注意:标识符开头不能是数字

  • 标识符不能是保留字和关键字。

  • 变量的名字是区分大小写的如: age和Age是不同的变量。在实际的运用中,也建议,不要用一个单词大小写区分两个变量。

  • 标识符(变量名称)一定要见名思意 :变量名称建议用名词,方法名称建议用动词


Dart的入口方法


Dart 入口方法main有两种定义


//表示main方法没有返回值
void main(){
print('dart');
}

main(){

print('dart');
}

Dart基本类型


数据类型


Dart中常用的数据类型有以下的类型:


Numbers(数值):
int
double
Strings(字符串)
String
Booleans(布尔)
bool
List(数组)
在Dart中,数组是列表对象,所以大多数人只是称它们为列表
Maps(字典)
通常来说,Map 是一个键值对相关的对象。 键和值可以是任何类型的对象。每个键只出现一次,而一个值则可以出现多次

数值类型: int double


int整型:


  int a=123;
a=45;

double既可以是整型,也可是浮点型:


double b=23.5;

b=24;

字符串类型


字符串定义:


var str1='this is str1';

String str2='this is str2';

字符串拼接:


print("$str1 $str2");

print(str1 + str2);

布尔类型


定义方式:


bool flag1=true;

var flag2=true;

判断条件上和Kotlin使用无异。


List(数组/集合)


不指定类型定义List


var list1 = ["张三",20,true];

print(list1);
print(list1[2]);

这就有点颠覆我们以往的观念了,一个list里面还可以有不同的类型。


指定类型定义List


var list2 = <String>["张三","李四"];

print(list2);

通过[]来定义Lsit


通过[]创建的集合的容量可以变化:


var list = [];

list.add("小明");
list.add(24);
list.add(true);

print(list);

也可以指定List中的元素类型:


List<String> list = [];

又或者是:


List<String> list = List.empty(growable: true);

growable 为 false 是为 固定长度列表,为 true 是为 长度可变列表


通过List.filled创建的集合长度是固定:


var list1 = List.filled(2, "");

var list2 = List<String>.filled(2, "");

Map


Map的定义:


直接赋值方式:


var person={
"name":"小明",
"age":28,
"work":["程序员","Android开发"]
};

print(person["name"]);

print(person["age"]);

print(person["work"]);


通过Map分别赋值:


var map =new Map();

map["name"]="小明";
map["age"]=26;
map["work"]=["程序员","Android开发"];
print(map);

is 关键词来判断类型


var str = 123;

if(str is String){
print('是string类型');
}else if(str is int){
print('int');
}else{
print('其他类型');
}

运算符


算术运算符


使用和符号上和Kotlin中的基本无异:


int a=13;
int b=5;

print(a+b); //加
print(a-b); //减
print(a*b); //乘
print(a/b); //除
print(a%b); //其余
print(a~/b); //取整

关系运算符


关系运算符主要有:


==    !=   >    <    >=    <=

使用:


int a=5;
int b=3;

print(a==b); //判断是否相等
print(a!=b); //判断是否不等
print(a>b); //判断是否大于
print(a<b); //判断是否小于
print(a>=b); //判断是否大于等于
print(a<=b); //判断是否小于等于

逻辑运算符


! 取反:


bool flag=false;
print(!flag); //取反

&&并且:全部为true的话值为true 否则值为false:


bool a=true;
bool b=true;

print(a && b);

||或者:全为false的话值为false 否则值为true:


bool a=false;
bool b=false;

print(a || b);

赋值运算符


基础赋值运算符 =、??= ++ --


int c=a+b;   //从右向左

b??=23;  表示如果b为空的话把 23赋值给b

++ --


// ++  --   表示自增 自减 1
//在赋值运算里面 如果++ -- 写在前面 这时候先运算 再赋值,如果++ --写在后面 先赋值后运行运算

var a = 10;
var b = a--;

print(a); //9
print(b); //10

// var a=10;

// a++; //a=a+1;

// print(a);

复合赋值运算符 +=、-= 、*= 、 /= 、%= 、~/=


+=


var a=12;
a+=12; //a = a+12
print(a);

-=


a-=6; // a = a-6

*=


a*=3;  //a=a*3;

/=


需要返回double类型


double a=12;
a/=12;

%=


double a=12;
a %= 12;

~/=


返回的是int整型


int a1 = 3;
int a2 = 2;

int a = a1 ~/= a2;

a = 1.


条件表达式


**if else **


 bool flag=true;

if(flag){
print('true');
}else{
print('false');
}

switch case


var sex = "女";
switch (sex) {
case "男":
print('性别是男');
break;
case "女":
print('性别是女');
break;
default:
print('传入参数错误');
break;
}

三目运算符


bool flag = false;
String str = flag?'我是true':'我是false';
print(str);

??运算符


var a;
var b= a ?? 10;

print(b); // a为空,则赋值为10

// var a=22;
// var b= a ?? 10;
//
// print(b); // 20

类型转换


Number与String类型之间的转换



  • Number类型转换成String类型toString()

  • String类型转成Number类型int.parse()


StringNumber


String str = '123';

var myNum = int.parse(str);

print(myNum is int);

// String str='123.1';

// var myNum=double.parse(str);

// print(myNum is double);


String:


var myNum=12;

var str=myNum.toString();

print(str is String);

其他类型转换成Boolean类型


isEmpty:判断字符串是否为空


var str = '';
if (str.isEmpty) {
print('str空');
} else {
print('str不为空');
}

isNaN:判断值是否为非数字


var myNum = 0 / 0;

if (myNum.isNaN) {
print('NaN');
}

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