注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

一个Android强大的饼状图

一、思路 1、空心图(一个大圆中心绘制一个小圆) 2、根据数据算出所占的角度 3、根据动画获取当前绘制的角度 4、根据当前角度获取Paint使用的颜色 5、动态绘制即将绘制的 和 绘制已经绘制的部分(最重要) 二、实现 1、空心图(一个大...
继续阅读 »

一、思路


  1、空心图(一个大圆中心绘制一个小圆)
2、根据数据算出所占的角度
3、根据动画获取当前绘制的角度
4、根据当前角度获取Paint使用的颜色
5、动态绘制即将绘制的 和 绘制已经绘制的部分(最重要)


二、实现


1、空心图(一个大圆中心绘制一个小圆)初始化数据


      paint = new Paint();
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.FILL_AND_STROKE);

screenW = DensityUtils.getScreenWidth(context);

int width = DensityUtils.dip2px(context, 15);//圆环宽度
int widthXY = DensityUtils.dip2px(context, 10);//微调距离

int pieCenterX = screenW / 2;//饼状图中心X
int pieCenterY = screenW / 3;//饼状图中心Y
int pieRadius = screenW / 4;// 大圆半径

//整个饼状图rect
pieOval = new RectF();
pieOval.left = pieCenterX - pieRadius;
pieOval.top = pieCenterY - pieRadius + widthXY;
pieOval.right = pieCenterX + pieRadius;
pieOval.bottom = pieCenterY + pieRadius + widthXY;

//里面的空白rect
pieOvalIn = new RectF();
pieOvalIn.left = pieOval.left + width;
pieOvalIn.top = pieOval.top + width;
pieOvalIn.right = pieOval.right - width;
pieOvalIn.bottom = pieOval.bottom - width;

//里面的空白画笔
piePaintIn = new Paint();
piePaintIn.setAntiAlias(true);
piePaintIn.setStyle(Paint.Style.FILL);
piePaintIn.setColor(Color.parseColor("#f4f4f4"));

2、根据数据算出所占的角度


使用递归保证cakeValues的值的总和必为100,然后根据值求出角度


   private void settleCakeValues(int i) {
float sum = getSum(cakeValues, i);
CakeValue value = cakeValues.get(i);
if (sum <= 100f) {
value.setItemValue(100f - sum);
cakeValues.set(i, value);
} else {
value.setItemValue(0);
settleCakeValues(i - 1);
}
}
复制代码

3、根据动画获取当前绘制的角度


curAngle就是当前绘制的角度,drawArc()就是绘制的方法


cakeValueAnimator.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float mAngle = obj2Float(animation.getAnimatedValue("angle"));
curAngle = mAngle;
drawArc();
}
});

4、根据当前角度获取Paint使用的颜色


根据当前的角度,计算当前是第几个item,通过
paint.setColor(Color.parseColor(cakeValues.get(colorIndex).getColors()));
来设置paint的颜色
复制代码

private int getCurItem(float curAngle) {
int res = 0;
for (int i = 0; i < itemFrame.length; i++) {
if (curAngle <= itemFrame[i] * ANGLE_NUM) {
res = i;
break;
}
}
return res;
}

5、动态绘制即将绘制的 和 绘制已经绘制的部分


最重要的一步,我的需求是4类,用不同的颜色




绘制当前颜色的扇形,curStartAngle扇形的起始位置,curSweepAngle扇形的终止位置


  paint.setColor(Color.parseColor(cakeValues.get(colorIndex).getColors()));
float curStartAngle = 0;
float curSweepAngle = curAngle;
if (curItem > 0) {
curStartAngle = itemFrame[curItem - 1] * ANGLE_NUM;
curSweepAngle = curAngle - (itemFrame[curItem - 1] * ANGLE_NUM);
}
canvas.drawArc(pieOval, curStartAngle, curSweepAngle, true, paint);

绘制已经绘制的扇形。根据curItem判断绘制过得扇形


for (int i = 0; i < curItem; i++) {
paint.setColor(Color.parseColor(cakeValues.get(i).getColors()));
if (i == 0) {
canvas.drawArc(pieOval, startAngle,(float) cakeValues.get(i).getItemValue() * ANGLE_NUM, true, paint);
continue;
}
canvas.drawArc(pieOval,itemFrame[i - 1] * ANGLE_NUM,(float) cakeValues.get(i).getItemValue() * ANGLE_NUM, true, paint);
}


绘制中心的圆


 canvas.drawArc(pieOvalIn, 0, 360, true, piePaintIn);

6、特别注意


isFirst判断是够是第一次绘制(绘制完成后,home键进入后台,再次进入,不需要动态绘制)


 @Override
protected void onDraw(Canvas canvas) {
if (isFirst && isDrawByAnim) {
drawCakeByAnim();
}
isFirst = false;
}
复制代码
isDrawByAnim判断是否需要动画绘制
drawCake()为静态绘制饼状图


public void surfaceCreated(SurfaceHolder holder) {
if (!isFirst||!isDrawByAnim)
drawCake();
}

更新


增加立体效果,提取配置参数


<declare-styleable name="CakeSurfaceView">
<attr name="isDrawByAnim" format="boolean"/>//是否动画
<attr name="isSolid" format="boolean"/>//是否立体
<attr name="duration" format="integer|reference"/>//动画时间
<attr name="defaultColor" format="string"/>//默认颜色

<attr name="ringWidth" format="integer|reference"/>//圆环宽度
<attr name="solidWidth" format="integer|reference"/>//立体宽度
<attr name="fineTuningWidth" format="integer|reference"/>//微调宽度
</declare-styleable>
复制代码
xml中使用
复制代码

<com.xp.xppiechart.view.CakeSurfaceView
android:id="@+id/assets_pie_chart"
android:background="#ffffff"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:defaultColor="#ff8712"
app:ringWidth="20"
app:solidWidth="5"
app:duration="3000"
app:isSolid="true"
app:isDrawByAnim="true"/>
复制代码


以上就是简单的实现动态绘制饼状图,待完善,以后会更新。如有建议和意见,请及时沟通。


代码下载:bobing107-CircularSectorProgressBar-master.zip

收起阅读 »

Android商品属性筛选与商品筛选!

前言这个次为大家带来的是一个完整的商品属性筛选与商品筛选。什么意思?都见过淘宝、京东等爱啪啪吧,里面有个商品详情,可以选择商品的属性,然后筛选出这个商品的具体型号,这样应该知道了吧?不知道也没关系,下面会有展示图。筛选属性最终完成关于商品筛选是有两种方式(至少...
继续阅读 »

前言

这个次为大家带来的是一个完整的商品属性筛选与商品筛选。什么意思?都见过淘宝、京东等爱啪啪吧,里面有个商品详情,可以选择商品的属性,然后筛选出这个商品的具体型号,这样应该知道了吧?不知道也没关系,下面会有展示图。

筛选属性最终完成
筛选属性最终完成

关于商品筛选是有两种方式(至少我只见到两种):

第一种: 将所有的商品的所有属性及详情返回给客户端,由客户端进行筛选。
淘宝用的就是这种。
第二种: 将所有的属性返回给客户端,客户选择完成属性后将属性发送给后台
,再由后台根据属性筛选出具体商品返回给客户端。
京东就是这样搞的。。

两种方式各有各的好处:

第一种:体验性特别好,用户感觉不到延迟,立即选中立即就筛选出了详情。就是客户端比较费劲。。。

第二种:客户端比较省时间,但是体验性太差了,你想想,在网络不是很通畅的时候,你选择一个商品还得等老半天。

因为当时我没有参加到这个接口的设计,导致一直在变化。。我才不会告诉不是后台不给力,筛选不出来才一股脑的将所有锅甩给客户端。

技术点

  1. 流式布局

     商品的属性并不是一样长的,所以需要自动适应内容的一个控件。
    推荐hongyang的博客。我就是照着那个搞的。
  2. RxJava

     不要问我,我不知道,我也是新手,我就是用它做出了效果,至于有没有
    用对,那我就不知道了。反正目的是达到了。
  3. Json解析???

准备

  1. FlowLayout
  2. RxJava

xml布局

这个部分的布局不是很难,只是代码量较多,咱们就省略吧,直接看效果吧

布局完成
布局完成

可以看到机身颜色、内存、版本下面都是空的,因为我们还没有将属性筛选出来。

数据分析

先看看整体的数据结构是怎么样的

数据结构
数据结构

每一个商品都有一个父类,仅作标识,不参与计算,比如数据中的华为P9就是一个商品的类目,在这下面有着各种属性组成的商品子类,这才是真正的商品。

而一个详细的商品是有三个基础属性所组成:

1. 版本
2. 内存
3. 制式

如上图中一个具体的商品的名称:"华为 P9全网通 3GB+32GB版 流光金 移动联通电信4G手机 双卡双待"

商品属性据结构
商品属性据结构

所以,要获得一个具体的商品是非常的简单,只需要客户选中的三个属性与上图中所对应的属性完全相同,就能得到这个商品。其中最关键的还是将所有的商品属性筛选出来。

筛选出所有属性及图片

本文中使用的数据是直接从Assets目录中直接读取的。

筛选出该商品的所有属性,怎么做呢?其实也是很简单的,直接for所有商品的所有属性,然后存储起来,去除重复的属性,那么最后剩下的就是该商品的属性了

 /**
* 初始化商品信息
*
  • 1. 提取所有的属性

  • *
  • 2. 提取所有颜色的照片

  • */

    private void initGoodsInfo() {
    //所有的颜色
    mColors = new ArrayList<>();
    //筛选过程中临时存放颜色
    mTempColors = new ArrayList<>();
    //所有的内存
    mMonerys = new ArrayList<>();
    //筛选过程中临时的内存
    mTempMonerys = new ArrayList<>();
    //所有的版本
    mVersions = new ArrayList<>();
    //筛选过程中的临时版本
    mTempVersions = new ArrayList<>();
    //获取到所有的商品
    shopLists = responseDto.getMsg().getChilds();
    callBack.refreshSuccess("¥" + responseDto.getPricemin() + " - " + responseDto.getPricemax(), responseDto.getMsg().getParent().getName());
    callBack.parentName(responseDto.getMsg().getParent().getName());
    //遍历商品
    Observable.from(shopLists)
    //转换对象 获取所有商品的属性集合
    .flatMap(childsEntity -> Observable.from(childsEntity.getAttrinfo().getAttrs()))
    .subscribe(attrsEntity -> {
    //判断颜色
    if (mActivity.getString(R.string.shop_color).equals(attrsEntity.getAttrname()) && !mTempColors.contains(attrsEntity.getAttrvalue())) {
    mColors.add(new TagInfo(attrsEntity.getAttrvalue()));
    mTempColors.add(attrsEntity.getAttrvalue());
    }
    //判断制式
    if (mActivity.getString(R.string.shop_standard).equals(attrsEntity.getAttrname()) && !mTempVersions.contains(attrsEntity.getAttrvalue())) {
    mVersions.add(new TagInfo(attrsEntity.getAttrvalue()));
    mTempVersions.add(attrsEntity.getAttrvalue());
    }
    //判断内存
    if (mActivity.getString(R.string.shop_monery).equals(attrsEntity.getAttrname()) && !mTempMonerys.contains(attrsEntity.getAttrvalue())) {
    mMonerys.add(new TagInfo(attrsEntity.getAttrvalue()));
    mTempMonerys.add(attrsEntity.getAttrvalue());
    }
    });

    // 提取出 每种颜色的照片
    tempImageColor = new ArrayList<>();
    mImages = new ArrayList<>();
    //遍历所有的商品列表
    Observable.from(shopLists)
    .subscribe(childsEntity -> {
    String color = childsEntity.getAttrinfo().getAttrs().get(0).getAttrvalue();
    if (!tempImageColor.contains(color)) {
    mImages.add(childsEntity.getShowimg());
    tempImageColor.add(color);
    }
    });
    // 提取出 每种颜色的照片

    //通知图片
    callBack.changeData(mImages, "¥" + responseDto.getPricemin() + " - " + responseDto.getPricemax());
    callBack.complete(null);
    }

    初始化属性列表

    属性之间是有一些关系的,比如我这里是以颜色为初始第一项,那么我就得根据颜色筛选出这个颜色下的所有内存,然后根据内存筛选出所有的版本。同时,只要颜色、内存、版本三个都选择了,就得筛选出这个商品。

    {颜色>内存>版本}>具体商品

    颜色

    初始化颜色,设置选择监听,一旦用户选择了某个颜色,那么需要获取这个颜色下的所有内存,并且要开始尝试获取商品详情。

    1. 初始化颜色

       /**
      * 初始化颜色
      *
      * @hint
      */

      private void initShopColor() {
      for (TagInfo mColor : mColors) {
      //初始化所有的选项为未选择状态
      mColor.setSelect(false);
      }
      tvColor.setText("\"未选择颜色\"");
      mColors.get(colorPositon).setSelect(true);
      colorAdapter = new ProperyTagAdapter(mActivity, mColors);
      rlShopColor.setAdapter(colorAdapter);
      colorAdapter.notifyDataSetChanged();
      rlShopColor.setTagCheckedMode(FlowTagLayout.FLOW_TAG_CHECKED_SINGLE);
      rlShopColor.setOnTagSelectListener((parent, selectedList) -> {
      colorPositon = selectedList.get(0);
      strColor = mColors.get(colorPositon).getText();
      // L.e("选中颜色:" + strColor);
      tvColor.setText("\"" + strColor + "\"");
      //获取颜色照片
      initColorShop();
      //查询商品详情
      iterationShop();
      });
      }
    2. 获取颜色下所有的内存和该颜色的照片

       /**
      * 初始化相应的颜色的商品 获得 图片
      */

      private void initColorShop() {
      //初始化 选项数据
      Observable.from(mMonerys).subscribe(tagInfo -> {
      tagInfo.setChecked(true);
      });
      L.e("开始筛选颜色下的内存----------------------------------------------------------------------------------");
      final List tempColorMemery = new ArrayList<>();
      //筛选内存
      Observable.from(shopLists)
      .filter(childsEntity -> childsEntity.getAttrinfo().getAttrs().get(0).getAttrvalue().equals(strColor))
      .flatMap(childsEntity -> Observable.from(childsEntity.getAttrinfo().getAttrs()))
      .filter(attrsEntity -> mActivity.getString(R.string.shop_monery).equals(attrsEntity.getAttrname()))
      .subscribe(attrsEntity -> {
      tempColorMemery.add(attrsEntity.getAttrvalue());
      // L.e("内存:"+attrsEntity.getAttrvalue());
      });

      Observable.from(mTempMonerys)
      .filter(s -> !tempColorMemery.contains(s))
      .subscribe(s -> {
      L.e("没有的内存:" + s);
      mMonerys.get(mTempMonerys.indexOf(s)).setChecked(false);
      });
      momeryAdapter.notifyDataSetChanged();
      L.e("筛选颜色下的内存完成----------------------------------------------------------------------------------");

      //获取颜色的照片
      ImageHelper.loadImageFromGlide(mActivity, mImages.get(tempImageColor.indexOf(strColor)), ivShopPhoto);
      }
    1. 根据选中的属性查询是否存在该商品

       /**
      * 迭代 选择商品属性
      */

      private void iterationShop() {
      // 选择的内存 选择的版本 选择的颜色
      if (strMemory == null || strVersion == null || strColor == null)
      return;
      //隐藏购买按钮 显示为缺货
      resetBuyButton(false);
      Observable.from(shopLists)
      .filter(childsEntity -> childsEntity.getAttrinfo().getAttrs().get(0).getAttrvalue().equals(strColor))
      .filter(childsEntity -> childsEntity.getAttrinfo().getAttrs().get(1).getAttrvalue().equals(strVersion))
      .filter(childsEntity -> childsEntity.getAttrinfo().getAttrs().get(2).getAttrvalue().equals(strMemory))
      .subscribe(childsEntity -> {
      L.e(childsEntity.getShopprice());
      tvPrice.setText("¥" + childsEntity.getShopprice());
      // ImageHelper.loadImageFromGlide(mActivity, Constant.IMAGE_URL + childsEntity.getShowimg(), ivShopPhoto);
      L.e("已找到商品:" + childsEntity.getName() + " id:" + childsEntity.getPid());
      selectGoods = childsEntity;
      tvShopName.setText(childsEntity.getName());
      //显示购买按钮
      resetBuyButton(true);
      initShopStagesCount++;
      });
      }

    内存

    通过前面一步,已经获取了所有的内存。这一步只需要展示该所有内存,设置选择监听,选择了某个内存后就根据 选择颜色>选择内存 获取所有的版本。并在在其中也是要iterationShop()查询商品的,万一你是往回点的时候呢?

    1. 初始化版本

       /**
      * 初始化内存
      */

      private void initShopMomery() {
      for (TagInfo mMonery : mMonerys) {
      mMonery.setSelect(false);
      Log.e(" ", "initShopMomery: " + mMonery.getText());
      }
      tvMomey.setText("\"未选择内存\"");
      mMonerys.get(momeryPositon).setSelect(true);
      //-----------------------------创建适配器
      momeryAdapter = new ProperyTagAdapter(mActivity, mMonerys);
      rlShopMomery.setAdapter(momeryAdapter);
      rlShopMomery.setTagCheckedMode(FlowTagLayout.FLOW_TAG_CHECKED_SINGLE);
      rlShopMomery.setOnTagSelectListener((parent, selectedList) -> {
      momeryPositon = selectedList.get(0);
      strMemory = mMonerys.get(momeryPositon).getText();
      // L.e("选中内存:" + strMemory);
      iterationShop();
      tvMomey.setText("\"" + strMemory + "\"");
      iterationVersion();
      });
      }
    2. 根据已选择的颜色和内存获取到版本

       /**
      * 迭代 获取版本信息
      */

      private void iterationVersion() {
      if (strColor == null || strMemory == null) {
      return;
      }
      // L.e("开始迭代版本");
      Observable.from(mVersions).subscribe(tagInfo -> {
      tagInfo.setChecked(true);
      });
      final List iterationTempVersion = new ArrayList<>();
      //1. 遍历出 这个颜色下的所有手机
      //2. 遍历出 这些手机的所有版本
      Observable.from(shopLists)
      .filter(childsEntity -> childsEntity.getAttrinfo().getAttrs().get(0).getAttrvalue().equals(strColor))
      .filter(childsEntity -> childsEntity.getAttrinfo().getAttrs().get(2).getAttrvalue().equals(strMemory))
      .flatMap(childsEntity -> Observable.from(childsEntity.getAttrinfo().getAttrs()))
      .filter(attrsEntity -> attrsEntity.getAttrname().equals(mActivity.getString(R.string.shop_standard)))
      .subscribe(attrsEntity -> {
      iterationTempVersion.add(attrsEntity.getAttrvalue());
      });

      Observable.from(mTempVersions).filter(s -> !iterationTempVersion.contains(s)).subscribe(s -> {
      mVersions.get(mTempVersions.indexOf(s)).setChecked(false);
      });
      versionAdapter.notifyDataSetChanged();
      // L.e("迭代版本完成");
      }

    版本

    其实到了这一步,已经算是完成了,只需要设置监听,获取选中的版本,然后开始查询商品。

        /**
    * 初始化版本
    */

    private void initShopVersion() {
    for (TagInfo mVersion : mVersions) {
    mVersion.setSelect(false);
    }
    tvVersion.setText("\"未选择版本\"");
    mVersions.get(versionPositon).setSelect(true);
    //-----------------------------创建适配器
    versionAdapter = new ProperyTagAdapter(mActivity, mVersions);
    rlShopVersion.setAdapter(versionAdapter);
    rlShopVersion.setTagCheckedMode(FlowTagLayout.FLOW_TAG_CHECKED_SINGLE);
    rlShopVersion.setOnTagSelectListener((parent, selectedList) -> {
    versionPositon = selectedList.get(0);
    strVersion = mVersions.get(versionPositon).getText();
    // L.e("选中版本:" + strVersion);
    iterationShop();
    tvVersion.setText("\"" + strVersion + "\"");
    });
    }

    完成

    最终效果图如下:

    筛选属性最终完成
    筛选属性最终完成

    不要在意后面的轮播图,那其实很简单的。

    代码下载:JiuYouYiShuSheng-Selector-master.zip

    收起阅读 »

    【开奖咯!】回帖晒晒端午节你们公司都发了什么?顺便抽个奖!~

    开奖咯!本次使用excel开奖,真实随机(参考链接https://www.excelhome.net/316.html)。部分用户回帖不符合活动要求,不参与本次开奖。参与回帖的10个随机幸运伙伴是:获得点赞最多的柳天明 5AuCf 4Lambert 3获得3...
    继续阅读 »
    开奖咯!本次使用excel开奖,真实随机(参考链接https://www.excelhome.net/316.html)。
    部分用户回帖不符合活动要求,不参与本次开奖。

    参与回帖的10个随机幸运伙伴是:


    获得点赞最多的

    柳天明 5
    AuCf 4
    Lambert 3

    获得3个最惨伙伴:

    yangjian、春春、孤狼☞小九

    请以上同学在6月17日 23:59前,将你的收件人,地址,电话,衣服图案(星空/字母)+尺码(L-3XL)信息发站内私信给@admin,超过领取截止时间未提交信息,视为放弃领取~

    感谢大家参与!下次见~

    =================================

    首先祝各位端午安康

    然而端午来临之际,各种群兴起了一些攀比之风

    有这样的



    还有这样的



    还有这样的



    然而我是这样的:




    不过节日没福利的同学们也没关系.环信精心为大家准备了端午福利 有福利的也可双喜临门!!!


    活动规则


    • 活动时间:即日起至 6 月 15 日 中午 12:00 截止
    • 参与方式 :在本篇帖子下留言关于端午福利或端午计划的回复(图文皆可,发图请单独开帖然后链接回到本帖下方)
    • 活动结束后,将从所有参与回帖的用户里随机抽取10人,赠送imgeek定制T恤。😉
    • 并且选出3个端午福利寒酸的盆友赠送夏日清凉挂脖风扇😆
    • 最多的前3名直接获得一件T恤!
    • 获奖名单将会在 6 月 15 日公布于本篇帖子下。
    T恤:



    收起阅读 »

    你还在用宏定义“iphoneX”判断安全区域(safe area)吗,教你正确使用Safe Area

    你还在用宏定义“iphone X”判断安全区域(safe area)吗,教你正确使用Safe Area。iOS 7 之后苹果给 UIViewController 引入了 topLayoutGuide 和 bottomLayoutGuide 两个属性来描述不希望...
    继续阅读 »

    你还在用宏定义“iphone X”判断安全区域(safe area)吗,教你正确使用Safe Area。
    iOS 7 之后苹果给 UIViewController 引入了 topLayoutGuide 和 bottomLayoutGuide 两个属性来描述不希望被透明的状态栏或者导航栏遮挡的最高位置(status bar, navigation bar, toolbar, tab bar 等)。这个属性的值是一个 length 属性( topLayoutGuide.length)。 这个值可能由当前的 ViewController 或者 NavigationController 或者 TabbarController 决定。

    1、一个独立的ViewController,不包含于任何其他的ViewController。如果状态栏可见,topLayoutGuide表示状态栏的底部,否则表示这个ViewController的上边缘。

    2、包含于其他ViewController的ViewController不对这个属性起决定作用,而是由容器ViewController决定这个属性的含义:

    3、如果导航栏(Navigation Bar)可见,topLayoutGuide表示导航栏的底部。

    4、如果状态栏可见,topLayoutGuide表示状态栏的底部。

    5、如果都不可见,表示ViewController的上边缘。

    6、这部分还比较好理解,总之是屏幕上方任何遮挡内容的栏的最底部。

    iOS 11 开始弃用了这两个属性, 并且引入了 Safe Area 这个概念。苹果建议: 不要把 Control 放在 Safe Area 之外的地方

    // These objects may be used as layout items in the NSLayoutConstraint API
    @available(iOS, introduced: 7.0, deprecated: 11.0)
    open var topLayoutGuide: UILayoutSupport {get}
    @available(iOS, introduced: 7.0, deprecated: 11.0)
    open var bottomLayoutGuide: UILayoutSupport { get}

    今天, 来研究一下 iOS 11 中新引入的这个 API。

    UIView 中的 safe area
    iOS 11 中 UIViewController 的 topLayoutGuide 和 bottonLayoutGuide 两个属性被 UIView 中的 safe area 替代了。

    open var safeAreaInsets: UIEdgeInsets {get}
    @available(iOS 11.0, *)
    open func safeAreaInsetsDidChange()

    safeAreaInsets

    这个属性表示相对于屏幕四个边的间距, 而不仅仅是顶部还有底部。这么说好像没有什么感觉, 我们来看一看这个东西分别在 iPhone X 和 iPhone 8 中是什么样的吧!

    什么都没有做, 只是新建了一个工程然后在 Main.storyboard 中的 UIViewController 中拖了一个橙色的 View 并且设置约束为:


    在 ViewController.swift 的 viewDidLoad 中打印

    override func viewDidLoad() {
    super.viewDidLoad()
    print(view.safeAreaInsets)
    }
    // 无论是iPhone 8 还是 iPhone X 输出结果均为
    // UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)


    iPhone 8 VS iPhone X Safe Area (竖屏)


    iPhone 8 VS iPhone X Safe Area (横屏)

    这样对比可以看出, iPhone X 同时具有上下, 还有左右的 Safe Area。

    **再来看这个例子: ** 拖两个自定义的 View, 这个 View 上有一个 显示很多字的Label。然后设置这两个 View 的约束分别是:

    let view1 = MyView()
    let view2 = MyView()
    view.addSubview(view1)
    view.addSubview(view2)
    let screenW = UIScreen.main.bounds.size.width
    let screenH = UIScreen.main.bounds.size.height
    view1.frame = CGRect(x: 0, y: 0, width:screenW, height: 200)
    view2.frame = CGRect( x: 0, y: screenH - 200, width:screenW, height: 200)


    可以看出来, 子视图被顶部的刘海以及底部的 home 指示区挡住了。我们可以使用 frame 布局或者 auto layout 来优化这个地方:

    let insets = UIApplication.shared.delegate?.window??.safeAreaInsets ?? UIEdgeInsets.zero  
    view1.frame = CGRect(x: insets.left,y: insets.top,width:view.bounds.width - insets.left - insets.right,height: 200)
    view2.frame = CGRect(x: insets.left,y: screenH - insets.bottom - 200,width:view.bounds.width - insets.left - insets.right,height: 200)


    这样起来好多了, 还有另外一个更好的办法是直接在自定义的 View 中修改 Label 的布局:

    override func layoutSubviews() {
    super.layoutSubviews()
    if #available(iOS 11.0, *) {
    label.frame = safeAreaLayoutGuide.layoutFrame
    }
    }


    这样, 不仅仅是在 ViewController 中能够使用 safe area 了。

    UIViewController 中的 safe area

    在 iOS 11 中 UIViewController 有一个新的属性

    @available(iOS 11.0, *)
    open var additionalSafeAreaInsets: UIEdgeInsets

    当 view controller 的子视图覆盖了嵌入的子 view controller 的视图的时候。比如说, 当 UINavigationController 和 UITabbarController 中的 bar 是半透明(translucent) 状态的时候, 就有 additionalSafeAreaInsets


    自定义的 View 上面的 label 布局兼容了 safe area。

    // UIView
    @available(iOS 11.0, *)
    open func safeAreaInsetsDidChange()
    //UIViewController
    @available(iOS 11.0, *)
    open func viewSafeAreaInsetsDidChange()

    这两个方法分别是 UIView 和 UIViewController 的 safe area insets 发生改变时调用的方法,如果需要做一些处理,可以重写这个方法。有点类似于 KVO 的意思。

    模拟 iPhone X 的 safe area


    额外的 safe area insets 也能用来测试你的 app 是否支持 iPhone X。在没有 iPhone X 也不方便使用模拟器的时候, 这个还是很有用的。

    //竖屏
    additionalSafeAreaInsets.top = 24.0
    additionalSafeAreaInsets.bottom = 34.0
    //竖屏, status bar 隐藏
    additionalSafeAreaInsets.top = 44.0
    additionalSafeAreaInsets.bottom = 34.0
    //横屏
    additionalSafeAreaInsets.left = 44.0
    additionalSafeAreaInsets.bottom = 21.0
    additionalSafeAreaInsets.right = 44.0

    UIScrollView 中的 safe area
    在 scroll view 上加一个 label。设置scroll 的约束为:

    scrollView.snp.makeConstraints { (make)  in
    make.edges.equalToSuperview()
    }


    iOS 7 中引入 UIViewController 的 automaticallyAdjustsScrollViewInsets 属性在 iOS11 中被废弃掉了。取而代之的是 UIScrollView 的 contentInsetAdjustmentBehavior

    @available(iOS 11.0 , *)
    public enum UIScrollViewContentInsetAdjustmentBehavior : Int {
    case automatic //default value
    case scrollableAxes
    case never
    case always
    }
    @available(iOS 11.0 , *)
    open var contentInsetAdjustmentBehavior: UIScrollViewContentInsetAdjustmentBehavior

    Content Insets Adjustment Behavior

    never 不做调整。

    scrollableAxes content insets 只会针对 scrollview 滚动方向做调整。

    always content insets 会针对两个方向都做调整。

    automatic 这是默认值。当下面的条件满足时, 它跟 always 是一个意思

    1、能够水平滚动,不能垂直滚动

    2、scroll view 是 当前 view controller 的第一个视图

    3、这个controller 是被navigation controller 或者 tab bar controller 管理的

    4、automaticallyAdjustsScrollViewInsets 为 true

    在其他情况下 automoatc 跟 scrollableAxes 一样

    Adjusted Content Insets

    iOS 11 中 UIScrollView 新加了一个属性: adjustedContentInset

    @available(iOS 11.0, *)
    open var adjustedContentInset: UIEdgeInsets {get}

    adjustedContentInset 和 contentInset 之间有什么区别呢?

    在同时有 navigation 和 tab bar 的 view controller 中添加一个 scrollview 然后分别打印两个值:

    //iOS 10
    //contentInset = UIEdgeInsets(top: 64.0, left: 0.0, bottom: 49.0, right: 0.0)
    //iOS 11
    //contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
    //adjustedContentInset = UIEdgeInsets(top: 64.0, left: 0.0, bottom: 49.0, right: 0.0)

    然后再设置:

    `// 给 scroll view 的四个方向都加 10 的间距`
    `scrollView.contentInset = UIEdgeInsets(top: ``10``, left: ``10``, bottom: ``10``, right: ``10``)`

    打印:

    //iOS 10
    //contentInset = UIEdgeInsets(top: 74.0, left: 10.0, bottom: 59.0, right: 10.0)
    //iOS 11
    //contentInset = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
    //adjustedContentInset = UIEdgeInsets(top: 74.0, left: 10.0, bottom: 59.0, right: 10.0)

    由此可见,在 iOS 11 中 scroll view 实际的 content inset 可以通过 adjustedContentInset 获取。这就是说如果你要适配 iOS 10 的话。这一部分的逻辑是不一样的。

    系统还提供了两个方法来监听这个属性的改变

    //UIScrollView
    @available(iOS 11.0, *)
    open func adjustedContentInsetDidChange()
    //UIScrollViewDelegate
    @available(iOS 11.0, *)
    optional public func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView)

    UITableView 中的 safe area

    我们现在再来看一下 UITableView 中 safe area 的情况。我们先添加一个有自定义 header 以及自定义 cell 的 tableview。设置边框为 self.view 的边框。也就是

    tableView.snp.makeConstraints { (make) in
    make.edges.equalToSuperview()
    }
    或者
    tableView.frame = view.bounds


    自定义的 header 上面有一个 lable,自定义的 cell 上面也有一个 label。将屏幕横屏之后会发现,cell 以及 header 的布局均自动留出了 safe area 以外的距离。cell 还是那么大,只是 cell 的 contnt view 留出了相应的距离。这其实是 UITableView 中新引入的属性管理的:

    @available(iOS 11.0, *)
    open var insetsContentViewsToSafeArea: Bool

    insetsContentViewsToSafeArea 的默认值是 true, 将其设置成 no 之后:


    可以看出来 footer 和 cell 的 content view 的大小跟 cell 的大小相同了。这就是说:在 iOS 11 下, 并不需要改变 header/footer/cell 的布局, 系统会自动区适配 safe area

    需要注意的是, Xcode 9 中使用 IB 拖出来的 TableView 默认的边框是 safe area 的。所以实际运行起来 tableview 都是在 safe area 之内的。


    UICollectionView 中的 safe area

    我们在做一个相同的 collection view 来看一下 collection view 中是什么情况:


    这是一个使用了 UICollectionViewFlowLayout 的 collection view。 滑动方向是竖向的。cell 透明, cell 的 content view 是白色的。这些都跟上面 table view 一样。header(UICollectionReusableView) 没有 content view 的概念, 所以给其自身设置了红色的背景。

    从截图上可以看出来, collection view 并没有默认给 header cell footer 添加safe area 的间距。能够将布局调整到合适的情况的方法只有将 header/ footer / cell 的子视图跟其 safe area 关联起来。跟 IB 中拖 table view 一个道理。


    现在我们再试试把布局调整成更像 collection view 那样:


    截图上可以看出来横屏下, 左右两边的 cell 都被刘海挡住了。这种情况下, 我们可以通过修改 section insets 来适配 safe area 来解决这个问题。但是再 iOS 11 中, UICollectionViewFlowLayout 提供了一个新的属性 sectionInsetReference 来帮你做这件事情。

    @available(iOS 11.0, *)
    public enum UICollectionViewFlowLayoutSectionInsetReference : Int {
    case fromContentInset
    case fromSafeArea
    case fromLayoutMargins
    }
    /// The reference boundary that the section insets will be defined as relative to. Defaults to .fromContentInset.

    /// NOTE: Content inset will always be respected at a minimum. For example, if the sectionInsetReference equals `.fromSafeArea`, but the adjusted content inset is greater that the combination of the safe area and section insets, then section content will be aligned with the content inset instead.
    @available(iOS 11.0, *)
    open var sectionInsetReference: UICollectionViewFlowLayoutSectionInsetReference

    可以看出来,系统默认是使用 .fromContentInset 我们再分别修改, 看具体会是什么样子的。

    fromSafeArea

    这种情况下 section content insets 等于原来的大小加上 safe area insets 的大小。

    跟使用 .fromLayoutMargins 相似使用这个属性 colection view 的 layout margins 会被添加到 section content insets 上面。


    IB 中的 Safe Area

    前面的例子都说的是用代码布局要实现的部分。但是很多人都还是习惯用 Interface Builder 来写 UI 界面。苹果在 WWDC 2107 Session 412 中提到:Storyboards 中的 safe area 是向下兼容的 也就是说, 即使在 iOS10 及以下的 target 中,也可以使用 safe area 来做布局。唯一需要做的就是给每个 stroyboard 勾选 Use Safe Area Layout Guide。实际测试看,应该是 iOS9 以后都只需要这么做。

    知识点: 在使用 IB 设置约束之后, 注意看相对的是 superview 还是 topLayoutGuide/bottomLayoutGuide, 包括在 Xcode 9 中勾选了 Use Safe Area Layout Guide 之后,默认应该是相对于 safe area 了。

    总结

    1、在适配 iPhone X 的时候首先是要理解 safe area 是怎么回事。盲目的 if iPhoneX{} 只会给之后的工作代码更多的麻烦。

    2、如果只需要适配到 iOS9 之前的 storyboard 都只需要做一件事情。

    3、Xcode9 用 IB 可以看得出来, safe area 到处都是了。理解起来很简单。就是系统对每个 View 都添加了 safe area, 这个区域的大小,是否跟 view 的大小相同是系统来决定的。在这个 View 上的布局只需要相对于 safe area 就可以了。每个 View 的 safe area 都可以通过 iOS 11 新增的 API safeAreaInsets 或者 safeAreaLayoutGuide 获取。

    4、对与 UIViewController 来说新增了 **additionalSafeAreaInsets **这个属性, 用来管理有 tabbar 或者 navigation bar 的情况下额外的情况。

    5、对于 UIScrollView, UITableView, UICollectionView 这三个控件来说,系统以及做了大多数的事情。

    6、scrollView 只需要设置 contentInsetAdjustmentBehavior 就可以很容易的适配带 iPhoneX

    7、tableView 只需要在 cell header footer 等设置约束的时候相对于 safe area 来做

    8、对 collection view 来说修改 sectionInsetReference 为 .safeArea 就可以做大多数的事情了。

    总的来说, safe area 可以看作是系统在所有的 view 上加了一个虚拟的 view, 这个虚拟的 view 的大小等都是跟 view 的位置等有关的(当然是在 iPhoneX上才有值) 以后在写代码的时候,自定义的控件都尽量针对 safe area 这个虚拟的 view 进行布局。
    文中有些图片都是从这里来的, 很多内容也跟这篇文章差不多 可能需要梯子

    参考文章 可能需要梯子

    作者:CepheusSun
    链接:http://www.jianshu.com/p/63c0b6cc66fd

    转自:https://www.jianshu.com/p/5bebc28e0ede

    收起阅读 »

    深度优先搜索和广度优先搜索

    不撞南墙不回头-深度优先搜索基础部分对于深度优先搜索和广度优先搜索,我很难形象的去表达它的定义。我们从一个例子来切入。输入一个数字n,输出1~n的全排列。即n=3时,输出123,132,213,231,312,321把问题形象化,假如有1,2,3三张扑克牌和编...
    继续阅读 »

    不撞南墙不回头-深度优先搜索

    基础部分

    对于深度优先搜索和广度优先搜索,我很难形象的去表达它的定义。我们从一个例子来切入。

    输入一个数字n,输出1~n的全排列。即n=3时,输出123,132,213,231,312,321

    把问题形象化,假如有1,2,3三张扑克牌和编号为1,2,3的三个箱子,把三张扑克牌分别放到三个箱子里有几种方法?

    我们用深度优先遍历搜索的思想来考虑这个问题。

    到1号箱子面前时,我们手里有1,2,3三种牌,我们把1放进去,然后走到2号箱子面签,手里有2,3两张牌, 然后我们把2放进去,再走到3号箱子前,手里之后3这张牌,所以把3放进去,然后再往前走到我们想象出来的一个4号箱子前,我们手里没牌了,所以,前面三个箱子中放牌的组合就是要输出的一种组合方式。(123)

    然后我们后退到3号箱子,把3这张拍取出来,因为这时我们手里只有一张牌,所以再往里放的话还是原来那种情况,所以我们还要再往后推,推到2号箱子前,把2从箱子中取出来,这时候我们手里有2,3两张牌,这时我们可以把3放进2号箱子,然后走到3号箱子中把2放进去,这又是一种要输出的组合方式.(132)

    就找这个思路继续下去再次回退的时候,我们就要退到1号箱,取出1,然后分别放2和3进去,然后产生其余的组合方式。

    有点啰嗦,但是基本是这么一个思路。

    我们来看一下实现的代码

    def sortNumber(self, n):
    flag = [False for i in range(n)]
    a = [0 for i in range(n)]
    l = []

    def dfs(step):
    if step == n:
    l.append(a[:])
    return
    for i in range(n):
    if flag[i] is False:
    flag[i] = True
    a[step] = i
    dfs(step + 1)
    flag[i] = False
    dfs(0)
    return l

    输出是

    [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]]

    我们创建的a这个list相当于上面说到的箱子,flag这个list呢,来标识某一个数字是否已经被用过了。

    其实主要的思想就这dfs方法里面的这个for循环中,在依次的排序中,我们默认优先使用最小的那个数字,这个for循环其实就代表了一个位置上有机会放所有的这些数字,这个flag标识就避免了在一个位置重复使用数字的问题。

    如果if 成立,说明当前位置可以使用这个数字,所以把这个数字放到a这个数组中,然后flag相同为的标识改为True,也就是说明这个数已经被占用了,然后在调用方法本身,进行下一步。

    flag[i] = False这句代码是很重要的,在上面的dfs(也就是下一步)结束之后,返回到当前这个阶段,我们必须模拟收回这个数字,也就是把flag置位False,表示这个数字又可以用了。

    思路大概就是这样子的,这就是深度优先搜索的一个简单的场景。用debug跟一下,一步一步的来看代码就更清晰的了。

    迷宫问题

    上面我们已经简单的了解了深度优先搜索,下面我们通过一个迷宫的问题来进一步数字这个算法,然后同时引出我们的广度优先搜索。

    迷宫是由m行n列的单元格组成,每个单元格要不是空地,要不就是障碍物,我们的任务是找到一条从起点到终点的最短路径。

    我们抽象成模型来看一下


    start代表起点,end代表终点,x代表障碍物也就是不能通过的点。

    首先我们来分析一下,从start(0,0)这个点,甚至说是每一个点出发,都有四个方向可以走,上下左右,仅对于(0,0)这个点来说,只能往右和下走,因为往左和上就到了单元格外面了,我们可以称之为越界了。

    我们用深度优先的思想来考虑的话,我们可以从出发点开始,全部都先往一个方向走,然后走到遇到障碍物或者到了边界的情况下,在改变另一个方向,然后再走到底,这样一直走下去。

    拿到我们这个题目中,我们可以这样来思考,在走的时候,我们规定一个右下左上这样的顺序,也就是先往右走,走到不能往右走的时候在变换方向。比如我们从(0,0)走到(0,1)这个点,在(0,1)这个点也是先往右走,但是我们发现(0,2)是障碍物,所以我们就改变为往下走,走到(1,1),然后在(1,1)开始也是先向右走,这样一直走下去,直到找到我们的目标点。

    其中我们要注意一点,在右下左上这四个方向中有一个方向是我们来时候的方向,在当前这个点,四个方向没有走完之前我们不要后退到上一个点,所以我们也需要一个像前面排数字代码里面的flag数组来记录当前位置时候被占用。我们必须是四个方向都走完了才能往后退到上一个换方向。

    下面我贴一下代码

    def depthFirstSearch(self):
    m = 5
    n = 4

    # 5行 4 列
    flag = [[False for i in range(n)] for j in range(m)]
    # 存储不能同行的位置
    a = [[False for i in range(n)] for j in range(m)]
    a[0][2] = True
    a[2][2] = True
    a[3][1] = True
    a[4][3] = True

    global min_step
    min_step = 99999

    director_l = [[0, 1], [1, 0], [0, -1], [-1, 0]]

    def dfs(x, y, step):

    # 什么情况下停止 (找到目标坐标)
    if x == 3 and y == 2:
    global min_step
    if step < min_step:
    min_step = step
    return

    # 右下左上
    for i in range(4):
    # 下一个点
    nextX = x + director_l[i][0]
    nextY = y + director_l[i][1]

    # 是否越界
    if nextX < 0 or nextX >= m or nextY < 0 or nextY >= n:
    continue

    # 不是障碍 and 改点还没有走过
    if a[x][y] is False and flag[x][y] is False:
    flag[x][y] = True
    dfs(nextX, nextY, step+1)
    flag[x][y] = False #回收

    dfs(0, 0, 0)
    return min_step

    首先flag这个算是二位数组吧,来记录我们位置是否占用了,然后a这个数组,是来记录整个单元格的,也就是标识那些障碍物的位置坐标。同样的,重点是这个dfs方法,他的参数x,y是指当前的坐标,step是步数。

    这个大家可以看到一个director_l的数组,他是来辅助我们根据当前左边和不同方向计算下一个位置的坐标的。

    dfs中我们已经注明了搜索停止的判断方式,也就是找到(3,2)这个点,然后下面的for循环,则代表四个不同的方向,每一个方向我们都会先求出他的位置,然后判断是否越界,如果没有越界在判断是否是障碍或者是否已经走过了,满足了所有的判断条件,我们在继续往下一个点,直到找到目标,比较路径的步数。

    这就是深度优先搜索了,当然,这个题目我们还有别的解法,这就到了我们说的广度优先搜索。

    层层递进-广度优先搜索

    我们先大体说一下广度优先搜索的思路,深度优先是先穷尽一个方向,而广度优先呢,则是基于一个位置,先拿到他所有能到达的位置,然后分别基于这些新位置,拿到他们能到达的所有位置,一次这样层层的递进,直到找到我们的终点。


    从(0,0)出发,可以到达(0,1)和(1,0),然后再从(0,1)出发到达(1,1),从(1,0)出发,到达(2,0)和(1,1),以此类推。

    所以我们我们维护一个队列来储存每一层遍历到达的点,当然了,不要重复储存同一个点。我们用一个指针head来标识当前的基准位置,也就是说最开始指向(0,0),当储存完毕所有(0,0)能抵达的位置时,我们就应该改变我们的基准位置了,这时候head++,就到了(0,1)这个位置,然后储存完他能到的所有位置,head++,就到了(1,0),然后继续。

    def breadthFirstSearch(self):

    class Node:
    def __init__(self):
    x = 0
    y = 0
    step = 0

    m, n = 5, 4
    # 记录
    flag = [[False for i in range(n)] for j in range(m)]

    # 储存地图信息
    a = [[False for i in range(n)] for j in range(m)]
    a[0][2] = True
    a[2][2] = True
    a[3][1] = True
    a[4][3] = True
    # 队列
    l = []
    startX, startY, step = 0, 0, 0
    head = 0
    index = 0

    node = Node()
    node.x = startX
    node.y = startY
    node.step = step
    index += 1
    l.append(node)
    flag[0][0] = True

    director_l = [[0, 1], [1, 0], [0, -1], [-1, 0]]

    while head < index:

    last_node = l[head]
    # 处理四个方向
    for i in range(4):

    # 当前位置
    currentX = last_node.x + director_l[i][0]
    currentY = last_node.y + director_l[i][1]

    # 找到目标
    if currentX == 4 and currentY == 2:
    print('step = ' + str(last_node.step + 1))
    return

    #是否越界
    if currentX < 0 or currentY < 0 or currentX >= m or currentY >= n:
    continue

    if a[currentX][currentY] is False and flag[currentX][currentY] is False:


    #不是目标
    flag[currentX][currentY] = True

    node_new = Node()
    node_new.x = currentX
    node_new.y = currentY
    node_new.step = last_node.step+1
    l.append(node_new)
    index += 1



    head += 1

    首先我们定义了一个节点Node的类,来封装节点位置和当前的步数,flag,a,director_l这两个数组作用跟深度优先搜索相同,l是我们维护的队列,head指针指向当前基准的那个位置的,index指针指向队列尾。首先我们先把第一个Node(也就是起点)存进队列,广度优先搜索不需要递归,只要加一个循环就行。

    每次走到符合要求的位置,我们便把他封装成Node来存进对列中,每存一个index都要+1.

    head指针必须在一个节点四个方向都处理完了之后才可以+1,变换下一个基准节点。

    小结

    简单的介绍了深度优先搜索和广度优先搜索,深度优先有一种先穷尽一个方向然后结合使用回溯来找到解,广度呢,可能就是每做一次操作就涵盖了所有的可能结果,然后一步步往后推出去,找到最后的解。这算我个人的理解吧,不准确也不官方,思想也只能算是稍有体会,还得继续努力。

    题外话

    碍于自己的算法基础太差,最近一直在做算法题,我是先刷了一段时间的题目,发现吃力了,才开始看的书。感觉有点本末倒置。其实应该是先看看书,把算法的一些常用大类搞清楚了,形成一个知识框架,这样在遇到问题的时候可以知道往那些方向上面思考,可能会好一些吧。

    链接:https://www.jianshu.com/p/9a6a65078fc2

    收起阅读 »

    AndroidRoom库基础入门

    一、前言     Room 是 Android Jetpack 的一部分。在 Android 中数据库是SQLite数据库,Room 就是在SQLite上面提供了一个抽象层,通过 Room 既能流畅地访问数据库,又能充...
    继续阅读 »


    一、前言


        Room 是 Android Jetpack 的一部分。在 Android 中数据库是SQLite数据库,Room 就是在SQLite上面提供了一个抽象层,通过 Room 既能流畅地访问数据库,又能充分展示 SQLite 数据库的强大功能。Room 主要有以下几大优点:



    • 在编译时校验 SQL 语句;

    • 易用的注解减少重复和易错的模板代码;

    • 简化的数据库迁移路径。


        正是 Room 有以上的优点,所以建议使用 Room 访问数据库。


    二、Room 主要组件


        Room 主要组件有三个:



    • 数据库类(RoomDatabase):拥有数据库,并作为应用底层持久性数据的主要访问接入点。

    • 数据实体类(Entity):表示应用数据库中的表。

    • 数据访问对象(DAO):提供方法使得应用能够在数据库中查询、更新、插入以及删除数据。


        应用从数据库类获取一个与之相关联的数据访问对象(DAO)。应用可以通过这个数据访问对象(DAO)在数据库中检索数据,并以相关联的数据实体对象呈现结果;应用也可以使用对的数据实体类对象,更新数据库对应表中的行(或者插入新行)。应用对数据库的操作完全通过 Room 这个抽象层实现,无需直接操作 SQLite数据库。下图就是 Room 各个组件之间的关系图:


    Room组件关系图


    三、Room 基础入门


        大致了解了 Room 的工作原理之后,下面我们就来介绍一下 Room 的使用入门。


    3.1 引入 Room 库到项目


    引入 Room 库到项目,在项目程序模块下的 build.gradle 文件的 dependencies


    // Kotlin 开发环境,需要引入 kotlin-kapt 插件
    apply plugin: 'kotlin-kapt'

    // .........

    dependencies {
    // other dependecies

    def room_version = "2.3.0"
    implementation("androidx.room:room-runtime:$room_version")
    // 使用 Kotlin 注解处理工具(kapt,如果项目使用Kotlin语言开发,这个必须引入,并且需要引入 kotlin-kapt 插件
    kapt("androidx.room:room-compiler:$room_version")
    // To use Kotlin Symbolic Processing (KSP)
    // ksp("androidx.room:room-compiler:$room_version")

    // 可选 - 为 Room 添加 Kotlin 扩展和协程支持
    implementation("androidx.room:room-ktx:$room_version")

    // 可选 - 为 Room 添加 RxJava2 支持
    implementation "androidx.room:room-rxjava2:$room_version"

    // 可选 - 为 Room 添加 RxJava3 支持
    implementation "androidx.room:room-rxjava3:$room_version"

    // optional - Guava support for Room, including Optional and ListenableFuture
    implementation "androidx.room:room-guava:$room_version"

    // optional - Test helpers
    testImplementation("androidx.room:room-testing:$room_version")
    }


    注意事项:如果项目是用 kotlin 语言开发,一定要引入 kotlin 注解处理工具,并且在 build.gradle 中添加 kitlin-kapt插件(apply plugin: 'kotlin-kapt'),否则应用运行会抛出 xx.AppDatabase. AppDatabase_Impl does not exist 异常。



    3.2 Room 使用示例


        使用 Room 访问数据库,需要首先定义 Room 的三个组件,然后通过数据访问对象实例访问数据。


    3.2.1 定义数据实体类


        数据实体类对应数据库中的表,实体类的字段对应表中的列。定义 Room 数据实体类,使用 data class 关键字,并使用 @Entity 注解标注。更多关于数据实体类相关注解(包括属性相关注解),请参考: Android Room 数据实体类详解。如下代码所示:


    @Entity
    class User(@PrimaryKey val uid: Int, @ColumnInfo() val name: String, @ColumnInfo val age: Int)


    注意事项:默认情况下,Room 会根据实体类的类为表名(在数据库中表名其实不区分大小写),开发者也可以在 @Entity 注解通过 tableName 参数指定表名。



    3.3.2 定义数据访问对象(DAO)


        数据访问对象是访问数据库的桥梁,通过 DAO 访问数据,查询或者更新数据库中的数据(数据实体类是媒介)。数据访问对象(DAO)是一个接口,定义时添加 @Dao 注解标注,接口中的每一个成员方法表示一个操作,成员方法使用注解标示操作类型。更多关于数据访问对象(DAO)和数据操作类型注解,请参考:Android Room 数据访问对象详解。以下是简单的 DAO 示例代码:


    @Dao
    interface UserDao {
    @Query("SELECT * FROM user")
    fun getAll(): List<User>

    @Query("SELECT * FROM user WHERE name LIKE :name")
    fun findByName(name: String): List<User>

    @Insert
    fun insertAll(vararg users: User)

    @Delete
    fun delete(user: User)
    }


    注意事项:
    1. 数据访问对象是接口类型,成员方法是没有方法体的,成员方法必须使用注解标示操作类型;
    2. 数据库实体类成员方法中的 SQL 语句,在编译是会检查语法是否正确。



    3.3.3 定义数据库类


        数据库是存储数据的地方,使用 Room 定义数据库时,声明一个抽象类(abstract class),并用 @Database 注解标示,在 @Database 注解中使用 entities 参数指定数据库关联的数据实体类列表,使用 version 参数指定数据的版本。数据库类中包含获取数据访问实体类对象的抽象方法,更多关于数据库相关内容,请参考:Android Room 数据库详解,以下是简单的数据类定义。


    @Database(entities = [User::class], version = 1)
    abstract class AppDatabase: RoomDatabase() {
    abstract fun userDao(): UserDao
    }


    注意事项:
    1. 数据库类是一个抽象类,他的成员方法是抽象方法;
    2. 定义数据库类时必须指定关联的数据实体类列表,这样数据库类才知道需要创建那些表;
    3. 数据的版本号,如果数据库的表构造有变动时,需要升级版本号,这样数据库才会更新表结构(如修改表字段、新增表等,跟直接使用 SQLite 接口使用 SQLiteDatabase 类一样),但是数据库的升级并不是修改版本号那么简单,还需要处理数据库升级过程中需要修改的地方,更多详情请参考:Android Room 数据库升级



    3.3.4 创建数据库实例


        定义好数据实体类、数据访问对象(DAO)和数据类之后,便可以创建数据库实例。使用 Room.databaseBuilder().build() 创建一个数据库实体类,Room 会根据定义的数据实体类、数据库访问对象和数据库类,以及他们定义时指定的对应关系,自动创建数据库和对应的表关系。如以下示例代码所示:


    val db = Room.databaseBuilder(applicationContext, AppDatabase::class.java, "app_db").build()


    注意事项:
    1. 每一个 RoomDatabase 实例都是非常耗费资源的,如果你的应用是单个进程中运行,那么在实例 RoomDatabase 时请遵循单例设计模式,在单个进程中几乎不需要访问多个 RoomDatabase 实例。
    2. 如果你的应用在多个进程中运行(比如:远程服务(RemoteService)),在构建 RoomDatabase 的构建器中调用 Room.databaseBuilder().enableMultiInstanceInvalidation() 方法,这样一来,在每个进程中都有一个 RoomDatabase 实例,如果在某个进程中将共享的数据库文件失效,将会自动将这个失效自动同步给其他进程中的 RoomDatabase 实例。



    3.3.5 从数据库实例中获取数据访问对象(DAO)实例


        在定义数据库类时,将数据访问对象(DAO)类与之相关联,定义抽象方法返回对应的数据库访问对象(DAO)实例。在数据库实例化过程中,Room 会自动生成对应的数据访问对象(DAO),只需要调用定义数据库类时定义的抽象方法,即可获取对应的数据访问对象(DAO)实例。如下示例所示:


    val userDao = db.userDao()

    3.3.6 通过数据访问对象(DAO)实例操作数据库


        获取到数据访问对象(DAO)实例,就可以调用数据库访问对象(DAO)类中定义的方法操作数据库了。如下示例所示:


    Thread {
    // 插入数据
    userDao.insertAll(
    User(1, "Student1", 18),
    User(2, "Student2", 18),
    User(3, "Student3", 17),
    User(4, "Student4", 19)
    )

    // 查询数据
    val result = userDao.getAll()

    result.forEach {
    println("Student: id = ${it.uid}, name = ${it.name}, age = ${it.age}")
    }
    }.start()


    注意事项:
    1. 使用数据访问对象(DAO)实例操作数据库时,不能再 UI 主线程中调用 DAO 接口,否则会抛出异常(java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.



    四、编后语


        Room 是非常强大易用的,可以减少数据库操作过程中的出错,因为所有的 SQL 语句都在编译是进行检查,如果存在错误,将会在编译时就显示错误信息。不仅如此,Room 还非常优秀地处理了多进程很多线程访问数据库的问题。




    ————————————————
    版权声明:本文为CSDN博主「精装机械师」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/yingaizhu/article/details/117514630

    收起阅读 »

    Android数据库—SQLite

    Android数据库—SQLite 不适合存储大规模数据 用来存储每一个用户各自的信息 在线查看数据库方法 Android Studio查看SQLite数据库方法大全 从前我使用的是stetho方法来查看数据库,因为是外国网站,所以需要翻...
    继续阅读 »


    Android数据库—SQLite



    • 不适合存储大规模数据

    • 用来存储每一个用户各自的信息


    在线查看数据库方法


    Android Studio查看SQLite数据库方法大全



    从前我使用的是stetho方法来查看数据库,因为是外国网站,所以需要翻墙,比较麻烦。


    如今最新版的Android Studio可以直接在里面查看数据库,无需别的了。




    • stetho使用



      • build.gradle文件中引入依赖

        implementation 'com.facebook.stetho:stetho:1.5.1'


      • 在需要操作数据库的Activity中加入以下语句

      Stetho.initializeWithDefaults(this);


      • 谷歌调试



    继承SQLiteOpenHelper的类,加载驱动



    继承SQLiteOpenHelper类,实现三个方法。



    • 构造函数

    • 建表方法:onCreate方法

    • 更新表方法:onUpgrade方法




    • MySQLiteOpenHelper


    package com.hnucm.androiddatabase;

    import android.content.Context;
    import android.database.sqlite.SQLiteDatabase;
    import android.database.sqlite.SQLiteOpenHelper;
    import androidx.annotation.Nullable;

    //加载数据库驱动
    //建立连接
    public class MySQLiteOpenHelper extends SQLiteOpenHelper {
    //构造方法
    //name -> 数据库名字
    public MySQLiteOpenHelper(@Nullable Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) {
    super(context, name, factory, version);
    }

    //建表
    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
    //建表语句 自增长 主键
    sqLiteDatabase.execSQL("create table products(id integer primary key autoincrement,name varchar(20),singleprice double,restnum integer) ");
    }

    //更新表
    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {

    }
    }

    在Activity中进行增删改查



    整个Activity都是用数据库,所以声明驱动和数据库为全局变量,方便使用。



    //加载驱动
    mySQLiteOpenHelper = new MySQLiteOpenHelper(MainActivity.this,
    "product",null,1);
    //得到数据库
    sqLiteDatabase = mySQLiteOpenHelper.getWritableDatabase();


    布局文件中设置四个按钮,进行增删改查操作。




    • 布局-------activity_main.xml


    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    tools:context=".MainActivity">


    <Button
    android:id="@+id/insert"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="增加一条商品信息"
    android:textSize="25sp"
    />


    <Button
    android:id="@+id/delete"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="删除一条商品信息"
    android:textSize="25sp"
    />


    <Button
    android:id="@+id/update"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="修改一条商品信息"
    android:textSize="25sp"
    />


    <Button
    android:id="@+id/select"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="查询一条商品信息"
    android:textSize="25sp"
    />


    </LinearLayout>


    • 总体逻辑代码-------MainActivity


    package com.hnucm.androiddatabase;

    import androidx.appcompat.app.AppCompatActivity;

    import android.content.ContentValues;
    import android.database.Cursor;
    import android.database.sqlite.SQLiteDatabase;
    import android.os.Bundle;
    import android.util.Log;
    import android.view.View;
    import android.widget.Button;

    public class MainActivity extends AppCompatActivity implements View.OnClickListener{
    //声明增删改查四个按钮
    Button addBtn;
    Button delBtn;
    Button updateBtn;
    Button selectBtn;
    //声明驱动
    MySQLiteOpenHelper mySQLiteOpenHelper;
    //声明数据库
    SQLiteDatabase sqLiteDatabase;
    //数据对象
    ContentValues contentValues;
    //增删改查条件变量
    String id;
    String name;

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

    //加载驱动
    mySQLiteOpenHelper = new MySQLiteOpenHelper(MainActivity.this,
    "product",null,1);
    //得到数据库
    sqLiteDatabase = mySQLiteOpenHelper.getWritableDatabase();

    //初始化四个按钮
    addBtn = findViewById(R.id.insert);
    delBtn = findViewById(R.id.delete);
    updateBtn = findViewById(R.id.update);
    selectBtn = findViewById(R.id.select);

    //点击四个按钮
    addBtn.setOnClickListener(this);
    delBtn.setOnClickListener(this);
    updateBtn.setOnClickListener(this);
    selectBtn.setOnClickListener(this);
    }

    //四个按钮的点击事件
    @Override
    public void onClick(View view) {
    switch (view.getId()){
    //增加数据
    case R.id.insert:
    //创建数据,使用ContentValues -> HashMap
    contentValues = new ContentValues();
    //自增长 主键 增加无需加入id
    //contentValues.put("id",1);
    contentValues.put("name","辣条");
    contentValues.put("singleprice",3.50);
    contentValues.put("restnum",12);
    //将创建好的数据对象加入数据库中的哪一个表
    sqLiteDatabase.insert("products",null,contentValues);
    break;
    //删除数据
    case R.id.delete:
    //删除条件
    id = "1";
    name = "辣条";
    //在哪张表里,根据条件删除
    sqLiteDatabase.delete("products","id = ? and name = ?",
    new String[]{id,name});
    break;
    //修改数据
    case R.id.update:
    //修改条件
    id = "2";
    //将满足条件的数据修改
    contentValues = new ContentValues();
    contentValues.put("name","薯片");
    //在数据库中修改
    sqLiteDatabase.update("products",contentValues,"id=?",
    new String[]{id});
    break;
    //查询所有数据
    case R.id.select:
    //采用cursor游标查询
    Cursor cursor = sqLiteDatabase.query("products",null,null,
    null,null,null,null);
    //游标下一个存在,即没有到最后
    while(cursor.moveToNext()){
    //每一条数据取出每一列
    int id = cursor.getInt(cursor.getColumnIndex("id"));
    name = cursor.getString(cursor.getColumnIndex("name"));
    double singleprice = cursor.getDouble(cursor.getColumnIndex("singleprice"));
    int restnum = cursor.getInt(cursor.getColumnIndex("restnum"));
    //打印数据
    Log.i("products","id:" + id + ",name:" + name + ",singleprice:"
    + singleprice + ",restnum:" + restnum);
    }
    break;
    }
    }
    }

    增加数据


    //创建数据,使用ContentValues -> HashMap
    contentValues = new ContentValues();
    //自增长 主键 增加无需加入id
    //contentValues.put("id",1);
    contentValues.put("name","辣条");
    contentValues.put("singleprice",3.50);
    contentValues.put("restnum",12);
    //将创建好的数据对象加入数据库中的哪一个表
    sqLiteDatabase.insert("products",null,contentValues);

    删除数据


    //删除条件
    id = "1";
    name = "辣条";
    //在哪张表里,根据条件删除
    sqLiteDatabase.delete("products","id = ? and name = ?",
    new String[]{id,name});

    修改数据


    //修改条件
    id = "2";
    //将满足条件的数据修改
    contentValues = new ContentValues();
    contentValues.put("name","薯片");
    //在数据库中修改
    sqLiteDatabase.update("products",contentValues,"id=?",
    new String[]{id});

    查询数据


    //采用cursor游标查询
    //没有查询条件,所以查询表中所有信息
    Cursor cursor = sqLiteDatabase.query("products",null,null,
    null,null,null,null);
    //游标下一个存在,即没有到最后
    while(cursor.moveToNext()){
    //每一条数据取出每一列
    int id = cursor.getInt(cursor.getColumnIndex("id"));
    name = cursor.getString(cursor.getColumnIndex("name"));
    double singleprice = cursor.getDouble(cursor.getColumnIndex("singleprice"));
    int restnum = cursor.getInt(cursor.getColumnIndex("restnum"));
    //打印数据
    Log.i("products","id:" + id + ",name:" + name + ",singleprice:"
    + singleprice + ",restnum:" + restnum);
    }
    收起阅读 »

    总是听到有人说AndroidX,到底什么是AndroidX?

    Android技术迭代更新很快,各种新出的技术和名词也是层出不穷。不知从什么时候开始,总是会时不时听到AndroidX这个名词,这难道又是什么新出技术吗?相信有很多朋友也会存在这样的疑惑,那么今天我就来写一篇科普文章,向大家介绍AndroidX的前世今生。An...
    继续阅读 »

    Android技术迭代更新很快,各种新出的技术和名词也是层出不穷。不知从什么时候开始,总是会时不时听到AndroidX这个名词,这难道又是什么新出技术吗?相信有很多朋友也会存在这样的疑惑,那么今天我就来写一篇科普文章,向大家介绍AndroidX的前世今生。

    Android系统在刚刚面世的时候,可能连它的设计者也没有想到它会如此成功,因此也不可能在一开始的时候就将它的API考虑的非常周全。随着Android系统版本不断地迭代更新,每个版本中都会加入很多新的API进去,但是新增的API在老版系统中并不存在,因此这就出现了一个向下兼容的问题。

    举个例子,当Android系统发布到3.0版本的时候,突然意识到了平板电脑的重要性,因此为了让Android可以更好地兼容平板,Android团队在3.0系统(API 11)中加入了Fragment功能。但是Fragment的作用并不只局限于平板,以前的老系统中也想使用这个功能该怎么办?于是Android团队推出了一个鼎鼎大名的Android Support Library,用于提供向下兼容的功能。比如我们每个人都熟知的support-v4库,appcompat-v7库都是属于Android Support Library的,这两个库相信任何做过Android开发的人都使用过。

    但是可能很多人并没有考虑过support-v4库的名字到底是什么意思,这里跟大家解释一下。4在这里指的是Android API版本号,对应的系统版本是1.6。那么support-v4的意思就是这个库中提供的API会向下兼容到Android 1.6系统。它对应的包名如下:

    类似地,appcompat-v7指的是将库中提供的API向下兼容至API 7,也就是Android 2.1系统。它对应的包名如下:

    可以发现,Android Support Library中提供的库,它们的包名都是以android.support.*开头的。

    但是慢慢随着时间的推移,什么1.6、2.1系统早就已经被淘汰了,现在Android官方支持的最低系统版本已经是4.0.1,对应的API版本号是15。support-v4、appcompat-v7库也不再支持那么久远的系统了,但是它们的名字却一直保留了下来,虽然它们现在的实际作用已经对不上当初命名的原因了。

    那么很明显,Android团队也意识到这种命名已经非常不合适了,于是对这些API的架构进行了一次重新的划分,推出了AndroidX。因此,AndroidX本质上其实就是对Android Support Library进行的一次升级,升级内容主要在于以下两个方面。

    第一,包名。之前Android Support Library中的API,它们的包名都是在android.support.*下面的,而AndroidX库中所有API的包名都变成了在androidx.*下面。这是一个很大的变化,意味着以后凡是android.*包下面的API都是随着Android操作系统发布的,而androidx.*包下面的API都是随着扩展库发布的,这些API基本不会依赖于操作系统的具体版本。

    第二,命名规则。吸取了之前命名规则的弊端,AndroidX所有库的命名规则里都不会再包含具体操作系统API的版本号了。比如,像appcompat-v7库,在AndroidX中就变成了appcompat库。

    一个AndroidX完整的依赖库格式如下所示:

    implementation 'androidx.appcompat:appcompat:1.0.2'

    了解了AndroidX是什么之后,现在你应该放轻松了吧?它其实并不是什么全新的东西,而是对Android Support Library的一次升级。因此,AndroidX上手起来也没有任何困难的地方,比如之前你经常使用的RecyclerView、ViewPager等等库,在AndroidX中都会有一个对应的版本,只要改一下包名就可以完全无缝使用,用法方面基本上都没有任何的变化。

    但是有一点需要注意,AndroidX和Android Support Library中的库是非常不建议混合在一起使用的,因为它们可能会产生很多不兼容的问题。最好的做法是,要么全部使用AndroidX中的库,要么全部使用Android Support Library中的库。

    而现在Android团队官方的态度也很明确,未来都会为AndroidX为主,Android Support Library已经不再建议使用,并会慢慢停止维护。另外,从Android Studio 3.4.2开始,我发现新建的项目已经强制勾选使用AndroidX架构了。

    那么对于老项目的迁移应该怎么办呢?由于涉及到了包名的改动,如果从Android Support Library升级到AndroidX需要手动去改每一个文件的包名,那可真得要改死了。为此,Android Studio提供了一个一键迁移的功能,只需要对着你的项目名右击 → Refactor → Migrate to AndroidX,就会弹出如下图所示的窗口。

    这里点击Migrate,Android Studio就会自动检查你项目中所有使用Android Support Library的地方,并将它们全部改成AndroidX中对应的库。另外Android Studio还会将你原来的项目备份成一个zip文件,这样即使迁移之后的代码出现了问题你还可以随时还原回之前的代码。


    版权声明:本文为CSDN博主「guolin」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/guolin_blog/article/details/97142065

    收起阅读 »

    ReactiveObjC看这里就够了

    1、什么是ReactiveObjCReactiveObjC是ReactiveCocoa系列的一个OC方面用得很多的响应式编程三方框架,其Swift方面的框架是(ReactiveSwift)。RAC用信号(类名为RACSignal)来代替和处理各种变量的变化和传...
    继续阅读 »

    1、什么是ReactiveObjC

    ReactiveObjC是ReactiveCocoa
    系列的一个OC方面用得很多的响应式编程三方框架,其Swift方面的框架是(ReactiveSwift)。RAC用信号(类名为RACSignal)来代替和处理各种变量的变化和传递。核心思路:创建信号->订阅信号(subscribeNext)->发送信号
    通过信号signals的传输,重新组合和响应,软件代码的编写逻辑思路将变得更清晰紧凑,有条理,而不再需要对变量的变化不断的观察更新。

    2、什么是函数响应式编程

    响应式编程是一种和事件流有关的编程模式,关注导致状态值改变的改变的行为事件,一系列事件组成了事件流,一系列事件是导致属性值发生变化的原因,非常类似于设计模式中的观察者模式。在网上流传一个非常经典的解释响应式编程的概念,在一般的程序开发中:a = b + c,赋值之后 b 或者 c 的值变化后,a 的值不会跟着变化,而响应式编程的目标就是,如果 b 或者 c 的数值发生变化,a 的数值会同时发生变化;

    3、ReactiveObjC的流程分析

    ReactiveObjC主要有三个关键类:
    1、RACSignal信号
    RACSignal 是各种信号的基类,其中RACDynamicSignal是用的最多的动态信号
    2、RACSubscriber订阅者
    RACSubscriber是实现了RACSubscriber协议的订阅者类,这个协议定义了4个必须实现的方法

    @protocol RACSubscriber <NSObject>
    @required
    - (void)sendNext:(nullable id)value; //常见
    - (void)sendError:(nullable NSError *)error; //常见
    - (void)sendCompleted; //常见
    - (void)didSubscribeWithDisposable:(RACCompoundDisposable *)disposable;
    @end

    RACSubscriber主要保存了三个block,跟三个常见的协议方法一一对应\

    @property (nonatomic, copy) void (^next)(id value);
    @property (nonatomic, copy) void (^error)(NSError *error);
    @property (nonatomic, copy) void (^completed)(void);

    3、RACDisposable清洁工
    RACDisposable主要是对资源的释放处理,其中使用RACDynamicSignal时,会创建一个RACCompoundDisposable管理清洁工对象。其内部定义了两个数组,一个是_inlineDisposables[2]固定长度2的A fast array,超出2个对象的长度由_disposables数组管理,_inlineDisposables数组速度快,两个数组都是线程安全的。

    4、ReactiveObjC导入工程的方式

    pod 'ReactiveObjC'

    5、ReactiveObjC的几种使用情况

    1、NSArray 数组遍历

    NSArray * array = @[@"1",@"2",@"3",@"4",@"5",@"6"];
    [array.rac_sequence.signal subscribeNext:^(id _Nullable x) {
    NSLog(@"数组内容:%@", x);
    }];

    2、NSArray快速替换数组中内容为99和单个替换数组内容,两个方法都不会改变原数组内容,操作完后都会生成一个新的数组,省去了创建可变数组然后遍历出来单个添加的步骤。

    NSArray * array = @[@"1",@"2",@"3",@"4",@"5",@"6"];
    /*
    NSArray * newArray = [[array.rac_sequence mapReplace:@"99"] array];
    NSLog(@"%@",newArray);
    */
    NSArray * newArray = [[array.rac_sequence map:^id _Nullable(id _Nullable value) {
    NSLog(@"原数组内容%@",value);
    return @"99";
    }] array];
    NSLog(@"%@",newArray);

    3、NSDictionary 字典遍历

    NSDictionary * dic = @{@"name":@"Tom",@"age":@"20"};
    [dic.rac_sequence.signal subscribeNext:^(id _Nullable x) {

    RACTupleUnpack(NSString *key, NSString * value) = x;//X为为一个元祖,RACTupleUnpack能够将key和value区分开
    NSLog(@"数组内容:%@--%@",key,value);
    }];

    4、UIButton 监听按钮的点击事件

    UIButton * btn = [UIButton buttonWithType:UIButtonTypeCustom];
    btn.frame = CGRectMake(100, 200, 100, 60);
    btn.backgroundColor = [UIColor blueColor];
    [self.view addSubview:btn];
    //监听点击事件
    [[btn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(__kindof UIControl * _Nullable x) {
    NSLog(@"%@",x);//x为一个button对象,别看类型为UIControl,继承关系UIButton-->UIControl-->UIView-->UIResponder-->NSObject
    }];

    5、UITextField 监听输入框的一些事件

    UITextField * textF = [[UITextField alloc]initWithFrame:CGRectMake(100, 100, 200, 40)];
    textF.placeholder = @"请输入内容";
    textF.textColor = [UIColor blackColor];
    [self.view addSubview:textF];
    //实时监听输入框中文字的变化
    [[textF rac_textSignal] subscribeNext:^(NSString * _Nullable x) {
    NSLog(@"输入框的内容--%@",x);
    }];
    //UITextField的UIControlEventEditingChanged事件,免去了KVO
    [[textF rac_signalForControlEvents:UIControlEventEditingChanged] subscribeNext:^(__kindof UIControl * _Nullable x) {
    NSLog(@"%@",x);
    }];
    //添加监听条件
    [[textF.rac_textSignal filter:^BOOL(NSString * _Nullable value) {
    return [value isEqualToString:@"100"];//此处为判断条件,当输入的字符为100的时候执行下面方法
    }]subscribeNext:^(NSString * _Nullable x) {
    NSLog(@"输入框的内容为%@",x);
    }];

    6、KVO 代替KVO来监听按钮frame的改变

    UIButton * loginBtn = [UIButton buttonWithType:UIButtonTypeCustom];
    loginBtn.frame = CGRectMake(100, 210, 100, 60);
    loginBtn.backgroundColor = [UIColor blueColor];
    [loginBtn setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
    [loginBtn setTitle:@"666" forState:UIControlStateNormal];
    //[loginBtn setTitle:@"111" forState:UIControlStateDisabled];
    [self.view addSubview:loginBtn];
    //监听点击事件
    [[loginBtn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(__kindof UIControl * _Nullable x) {
    NSLog(@"%@",x);//x为一个button对象,别看类型为UIControl,继承关系UIButton-->UIControl-->UIView-->UIResponder-->NSObject
    x.frame = CGRectMake(100, 210, 200, 300);
    }];
    //KVO监听按钮frame的改变
    [[loginBtn rac_valuesAndChangesForKeyPath:@"frame" options:(NSKeyValueObservingOptionNew) observer:self] subscribeNext:^(RACTwoTuple<id,NSDictionary *> * _Nullable x) {
    NSLog(@"frame改变了%@",x);
    }];

    //下面方法也能监听,但是在按钮创建的时候此方法也执行了,简单说就是在界面展示之前此方法就走了一遍,总感觉怪怪的。
    /*
    [RACObserve(loginBtn, frame) subscribeNext:^(id _Nullable x) {
    NSLog(@"frame改变了%@",x);
    }];

    7、NSNotification 监听通知事件

    [[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIKeyboardDidShowNotification object:nil] subscribeNext:^(NSNotification * _Nullable x) {
    NSLog(@"监听键盘弹出"); //不知道为啥此方法不止走一次,但是原本的通知监听方法只走一次,有知道的可以私信我,谢谢
    }];

    8、timer 代替timer定时循环执行方法

    [[RACSignal interval:2.0 onScheduler:[RACScheduler mainThreadScheduler]] subscribeNext:^(NSDate * _Nullable x) {
    //这里面的方法2秒一循环
    }];
    //如果关闭定时器,停止需要创建一个全局的disposable
    //@property (nonatomic, strong) RACDisposable * disposable;//创建
    /*
    self.disposable = [[RACSignal interval:2.0 onScheduler:[RACScheduler mainThreadScheduler]] subscribeNext:^(NSDate * _Nullable x) {
    NSLog(@"当前时间:%@", x); // x 是当前的系统时间
    //关闭计时器
    [self.disposable dispose];
    }];
    */

    6、开发中用到的小栗子

    1、发送短信验证码的按钮倒计时

    /*
    @property (nonatomic, strong) RACDisposable * disposable;
    @property (nonatomic, assign) NSInteger time;
    */
    //上面两句要提前定义
    UIButton *btn = [[UIButton alloc]initWithFrame:CGRectMake(100, 200, 200, 50)];
    btn.titleLabel.textAlignment = NSTextAlignmentCenter;
    btn.backgroundColor = [UIColor greenColor];
    [btn setTitle:@"发送验证码" forState:(UIControlStateNormal)];
    [self.view addSubview:btn];
    [[btn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(__kindof UIControl * _Nullable x) {
    self.time = 10;
    btn.enabled = NO;
    [btn setTitle:[NSString stringWithFormat:@"请稍等%zd秒",self.time] forState:UIControlStateDisabled];
    self.disposable = [[RACSignal interval:1.0 onScheduler:[RACScheduler mainThreadScheduler]] subscribeNext:^(NSDate * _Nullable x) {
    //减去时间
    self.time --;
    //设置文本
    NSString *text = (self.time > 0) ? [NSString stringWithFormat:@"请稍等%zd秒",_time] : @"重新发送";
    if (self.time > 0) {
    btn.enabled = NO;
    [btn setTitle:text forState:UIControlStateDisabled];
    }else{
    btn.enabled = YES;
    [btn setTitle:text forState:UIControlStateNormal];
    //关掉信号
    [self.disposable dispose];
    }
    }];
    }];

    2、登录按钮的状态根据账号和密码输入框内容的长度来改变

    UITextField *userNameTF = [[UITextField alloc]initWithFrame:CGRectMake(100, 70, 200, 50)];
    UITextField *passwordTF = [[UITextField alloc]initWithFrame:CGRectMake(100, 130, 200, 50)];
    userNameTF.placeholder = @"请输入用户名";
    passwordTF.placeholder = @"请输入密码";
    [self.view addSubview:userNameTF];
    [self.view addSubview:passwordTF];
    UIButton *loginBtn = [[UIButton alloc]initWithFrame:CGRectMake(40, 180, 200, 50)];
    [loginBtn setTitle:@"马上登录" forState:UIControlStateNormal];
    [self.view addSubview:loginBtn];
    //根据textfield的内容来改变登录按钮的点击可否
    RAC(loginBtn, enabled) = [RACSignal combineLatest:@[userNameTF.rac_textSignal, passwordTF.rac_textSignal] reduce:^id _Nullable(NSString * username, NSString * password){
    return @(username.length >= 11 && password.length >= 6);
    }];
    //根据textfield的内容来改变登录按钮的背景色
    RAC(loginBtn, backgroundColor) = [RACSignal combineLatest:@[userNameTF.rac_textSignal, passwordTF.rac_textSignal] reduce:^id _Nullable(NSString * username, NSString * password){
    return (username.length >= 11 && password.length >= 6) ? [UIColor redColor] : [UIColor grayColor];
    }];

    结尾:
    本文参考:

    关于ReactiveObjC原理及流程简介https://www.jianshu.com/p/fecbe23d45c1

    响应式编程之ReactiveObjC常见用法https://www.jianshu.com/p/6af75a449d90

    【iOS 开发】ReactiveObjC(RAC)的使用汇总

    https://www.jianshu.com/p/0845b1a07bfa

    链接:https://www.jianshu.com/p/222c21007251

    收起阅读 »

    提升用户愉悦感的润滑剂-看SDWebImage本地缓存结构设计

    手机应用发展到今天,用户的体验至关重要,有时决定着应用产品的生死,比如滑动一个商品列表时,用户自然地希望列表的滑动跟随手指,如丝般顺滑,如果卡顿,不耐烦的用户就会点退出按钮,商品也就失去了展示机会;而当一个用户发现自己装了某个APP后流量用的特别快,Ta可能会...
    继续阅读 »

    手机应用发展到今天,用户的体验至关重要,有时决定着应用产品的生死,比如滑动一个商品列表时,用户自然地希望列表的滑动跟随手指,如丝般顺滑,如果卡顿,不耐烦的用户就会点退出按钮,商品也就失去了展示机会;
    而当一个用户发现自己装了某个APP后流量用的特别快,Ta可能会永远将这个APP打入冷宫。想要优化界面的响应、节省流量,本地缓存对用户而言是透明的,却是必不可少的一环。
    设计本地缓存并不是开一个数组或本地数据库,把数据丢进去就能达到预期效果的,这是因为:

    1、内存读写快,但容量有限,图片容易丢失;
    2、磁盘容量大,图片“永久”保存,但读写较慢。

    这对计算机与生俱来的矛盾,导致缓存设计必须将两种存储方式组合使用,加上iOS系统平台特性,无形中增加了本地缓存系统的复杂度,本篇来看看 SDWebImage 是如何实现一个流畅的缓存系统的。

    SDWebImage 本地缓存的整体流程如下:


    缓存数据的格式

    在深入具体的读写流程之前,先了解一下存储数据的格式,这有助于我们理解后续的操作步骤:

    1、为了加快界面显示的需要,内存缓存的图片用 UIImage;
    2、磁盘缓存的是 NSData,是从网络下载到的原始数据。

    写入流程

    存入图片时,调用入口方法:

    - (void)storeImage:(nullable UIImage *)image
    imageData:(nullable NSData *)imageData
    forKey:(nullable NSString *)key
    toDisk:(BOOL)toDisk
    completion:(nullable SDWebImageNoParamsBlock)completionBlock

    先写入 SDMemoryCache :

    [self.memCache setObject:image forKey:key cost:cost];

    再写入磁盘,由 ioQueue 异步执行:

    - (void)_storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key

    读取流程

    读取图片时,调用入口方法为:

    - (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock

    首先从内存缓存中获取:

    UIImage *image = [self imageFromMemoryCacheForKey:key];

    如果内存中有,直接返回给外部调用者;当内存缓存获取失败时,从磁盘获取图片文件数据:

    NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];

    解码为 UIImage:

    diskImage = [self diskImageForKey:key data:diskData options:options];

    并写回内存缓存,再返回给调用者。

    磁盘缓存

    磁盘缓存位于沙盒的 Caches 目录
    下:/Library/Caches/default/com.hackemist.SDWebImageCache.default/,
    保证了缓存图片在下次启动还存在,又不会被iTunes备份。
    文件名由cachedFileNameForKey生成,使用Key(即图片URL)的MD5值,顺便说明一下,图片的Key还有其他作用:

    1、作为获取缓存的索引
    2、防止重复写入

    写入过程很简单:

    - (void)_storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key

    利用 NSData 的文件写入方法:

    [imageData writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];

    内存缓存

    SDMemoryCache 是继承 NSCache 实现的,占用空间是用像素值来统计的(SDCacheCostForImage),因为 NSCache 的totalCostLimit 并不严格(关于 NSCache 的一些特性,请参考被忽视和误解的NSCache),用像素计算可以方
    便预估和加快运算。

    辅助内存缓存 weakCache

    你可能从看前面流程图时,就好奇这个辅助内存缓存的作用是什么,这是由于收到内存警告时,NSCache 里的图片可能已经被系统清除,但实际图片还是被界面上的 ImageView 保留着,因此在 weakCache 再保存一份,遇到这种情况时,只要简单地将 weakCache 中的值写回 NSCache 即可,这样提高了缓存命中率,也避免在界面保有图片时,缓存系统的误判,导致重复下载或从磁盘加载图片。
    weakCache 由 NSMapTable 实现,因为普通的NSDictionary无法分别对Key强引用,对值弱引用,即 weakCache 利用对 UIImage 的弱引用,可以判断是否被缓存以外的对象使用,是本地缓存加倍顺滑的关键喔。

    总结

    SDMemoryCache 的本地缓存很好地平衡了内存和磁盘的优缺点,最大限度利用了系统本身提供的 NSCache 和 NSData 的原生方法,巧妙地利用 weak 属性判断 UIImage 是否被引用问题,为我们开发提供了值得借鉴的思路。

    链接:https://www.jianshu.com/p/49ceb5f58590

    收起阅读 »

    几句代码轻松拥有扫码功能!

    ZXingLite for Android 是ZXing的精简版,基于ZXing库优化扫码和生成二维码/条形码功能,扫码界面完全支持自定义,也可一行代码使用默认实现的扫码功能。总之你想要的都在这里。简单如斯,你不试试? Come on~ViewfinderVi...
    继续阅读 »

    ZXingLite for Android 是ZXing的精简版,基于ZXing库优化扫码和生成二维码/条形码功能,扫码界面完全支持自定义,也可一行代码使用默认实现的扫码功能。总之你想要的都在这里。

    简单如斯,你不试试? Come on~

    ViewfinderView属性说明

    属性值类型默认值说明
    maskColorcolor#60000000扫描区外遮罩的颜色
    frameColorcolor#7F1FB3E2扫描区边框的颜色
    cornerColorcolor#FF1FB3E2扫描区边角的颜色
    laserColorcolor#FF1FB3E2扫描区激光线的颜色
    labelTextstring扫描提示文本信息
    labelTextColorcolor#FFC0C0C0提示文本字体颜色
    labelTextSizedimension14sp提示文本字体大小
    labelTextPaddingdimension24dp提示文本距离扫描区的间距
    labelTextWidthdimension提示文本的宽度,默认为View的宽度
    labelTextLocationenumbottom提示文本显示位置
    frameWidthdimension扫码框宽度
    frameHeightdimension扫码框高度
    laserStyleenumline扫描激光的样式
    gridColumninteger20网格扫描激光列数
    gridHeightinteger40dp网格扫描激光高度,为0dp时,表示动态铺满
    cornerRectWidthdimension4dp扫描区边角的宽
    cornerRectHeightdimension16dp扫描区边角的高
    scannerLineMoveDistancedimension2dp扫描线每次移动距离
    scannerLineHeightdimension5dp扫描线高度
    frameLineWidthdimension1dp边框线宽度
    scannerAnimationDelayinteger20扫描动画延迟间隔时间,单位:毫秒
    frameRatiofloat0.625f扫码框与屏幕占比
    framePaddingLeftdimension0扫码框左边的内间距
    framePaddingTopdimension0扫码框上边的内间距
    framePaddingRightdimension0扫码框右边的内间距
    framePaddingBottomdimension0扫码框下边的内间距
    frameGravityenumcenter扫码框对齐方式

    引入

    Gradle:

    最新版本

    //AndroidX 版本
    implementation 'com.king.zxing:zxing-lite:2.0.3'

    v1.x 旧版本

    //AndroidX 版本
    implementation 'com.king.zxing:zxing-lite:1.1.9-androidx'

    //Android Support 版本
    implementation 'com.king.zxing:zxing-lite:1.1.9'
    如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的JitPack来compile)
    allprojects {
    repositories {
    //...
    maven { url 'https://dl.bintray.com/jenly/maven' }
    }
    }

    版本说明

    v2.x 基于CameraX重构震撼发布

    v2.x 相对于 v1.x 的优势

    • v2.x基于CameraX,抽象整体流程,可扩展性更高。
    • v2.x基于CameraX通过预览裁剪的方式确保预览界面不变形,无需铺满屏幕,就能适配(v1.x通过遍历Camera支持预览的尺寸,找到与屏幕最接近的比例,减少变形的可能性(需铺满屏幕,才能适配))

    v2.x 特别说明

    • v2.x如果您是通过继承CaptureActivity或CaptureFragment实现扫码功能,那么动态权限申请相关都已经在CaptureActivity或CaptureFragment处理好了。
    • v2.x如果您是通过继承CaptureActivity或CaptureFragment实现扫码功能,如果有想要修改默认配置,可重写initCameraScan方法,修改CameraScan的配置即可,如果无需修改配置,直接在跳转原界面的onActivityResult 接收扫码结果即可(更多具体详情可参见app中的使用示例)。
    关于CameraX
    • CameraX暂时还是Beta版,可能会存在一定的稳定性,如果您有这个考量,可以继续使用 ZXingLite 以前的 v1.x 版本。相信不久之后CameraX就会发布稳定版。

    v1.x 说明

    【v1.1.9】 如果您正在使用 1.x 版本请点击下面的链接查看分支版本,当前 2.x 版本已经基于 Camerx 进行重构,不支持升级,请在新项目中使用。

    查看AndroidX版 1.x 分支 请戳此处

    查看Android Support版 1.x 分支 请戳此处

    查看 1.x API帮助文档

    使用 v1.x 版本的无需往下看了,下面的示例和相关说明都是针对于当前最新版本。

    示例

    布局示例

    可自定义布局(覆写getLayoutId方法),布局内至少要保证有PreviewView。

    PreviewView 用来预览,布局内至少要保证有PreviewView,如果是继承CaptureActivity或CaptureFragment,控件id可覆写getPreviewViewId方法自定义

    ViewfinderView 用来渲染扫码视图,给用户起到一个视觉效果,本身扫码识别本身没有关系,如果是继承CaptureActivity或CaptureFragment,控件id可复写getViewfinderViewId方法自定义,默认为previewView,返回0表示无需ViewfinderView

    ivFlashlight 用来内置手电筒,如果是继承CaptureActivity或CaptureFragment,控件id可复写getFlashlightId方法自定义,默认为ivFlashlight。返回0表示无需内置手电筒。您也可以自己去定义

    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <androidx.camera.view.PreviewView
    android:id="@+id/previewView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>
    <com.king.zxing.ViewfinderView
    android:id="@+id/viewfinderView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>
    <ImageView
    android:id="@+id/ivFlashlight"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:src="@drawable/zxl_flashlight_selector"
    android:layout_marginTop="@dimen/zxl_flashlight_margin_top" />
    </FrameLayout>

    或在你的布局中添加

        <include layout="@layout/zxl_capture"/>

    代码示例 (二维码/条形码)

        //跳转的默认扫码界面
    startActivityForResult(new Intent(context,CaptureActivity.class),requestCode);

    //生成二维码
    CodeUtils.createQRCode(content,600,logo);
    //生成条形码
    CodeUtils.createBarCode(content, BarcodeFormat.CODE_128,800,200);
    //解析条形码/二维码
    CodeUtils.parseCode(bitmapPath);
    //解析二维码
    CodeUtils.parseQRCode(bitmapPath);

    CameraScan配置示例

        //获取CameraScan,扫码相关的配置设置。CameraScan里面包含部分支持链式调用的方法,即调用返回是CameraScan本身的一些配置建议在startCamera之前调用。
    getCameraScan().setPlayBeep(true)//设置是否播放音效,默认为false
    .setVibrate(true)//设置是否震动,默认为false
    .setCameraConfig(new CameraConfig())//设置相机配置信息,CameraConfig可覆写options方法自定义配置
    .setNeedAutoZoom(false)//二维码太小时可自动缩放,默认为false
    .setNeedTouchZoom(true)//支持多指触摸捏合缩放,默认为true
    .setDarkLightLux(45f)//设置光线足够暗的阈值(单位:lux),需要通过{@link #bindFlashlightView(View)}绑定手电筒才有效
    .setBrightLightLux(100f)//设置光线足够明亮的阈值(单位:lux),需要通过{@link #bindFlashlightView(View)}绑定手电筒才有效
    .bindFlashlightView(ivFlashlight)//绑定手电筒,绑定后可根据光线传感器,动态显示或隐藏手电筒按钮
    .setOnScanResultCallback(this)//设置扫码结果回调,需要自己处理或者需要连扫时,可设置回调,自己去处理相关逻辑
    .setAnalyzer(new MultiFormatAnalyzer(new DecodeConfig()))//设置分析器,DecodeConfig可以配置一些解码时的配置信息,如果内置的不满足您的需求,你也可以自定义实现,
    .setAnalyzeImage(true)//设置是否分析图片,默认为true。如果设置为false,相当于关闭了扫码识别功能
    .startCamera();//启动预览


    //设置闪光灯(手电筒)是否开启,需在startCamera之后调用才有效
    getCameraScan().enableTorch(torch);

    CameraScan配置示例(只需识别二维码的配置示例)

            //初始化解码配置
    DecodeConfig decodeConfig = new DecodeConfig();
    decodeConfig.setHints(DecodeFormatManager.QR_CODE_HINTS)//如果只有识别二维码的需求,这样设置效率会更高,不设置默认为DecodeFormatManager.DEFAULT_HINTS
    .setFullAreaScan(false)//设置是否全区域识别,默认false
    .setAreaRectRatio(0.8f)//设置识别区域比例,默认0.8,设置的比例最终会在预览区域裁剪基于此比例的一个矩形进行扫码识别
    .setAreaRectVerticalOffset(0)//设置识别区域垂直方向偏移量,默认为0,为0表示居中,可以为负数
    .setAreaRectHorizontalOffset(0);//设置识别区域水平方向偏移量,默认为0,为0表示居中,可以为负数

    //在启动预览之前,设置分析器,只识别二维码
    getCameraScan()
    .setVibrate(true)//设置是否震动,默认为false
    .setAnalyzer(new MultiFormatAnalyzer(decodeConfig));//设置分析器,如果内置实现的一些分析器不满足您的需求,你也可以自定义去实现

    如果直接使用CaptureActivity需在您项目的AndroidManifest中添加如下配置

        <activity
    android:name="com.king.zxing.CaptureActivity"
    android:screenOrientation="portrait"
    android:theme="@style/CaptureTheme"/>

    快速实现扫码有以下几种方式:

    1、直接使用CaptureActivity或者CaptureFragment。(纯洁的扫码,无任何添加剂)

    2、通过继承CaptureActivity或者CaptureFragment并自定义布局。(适用于大多场景,并无需关心扫码相关逻辑,自定义布局时需覆写getLayoutId方法)

    3、在你项目的Activity或者Fragment中实例化一个CameraScan即可。(适用于想在扫码界面写交互逻辑,又因为项目架构或其它原因,无法直接或间接继承CaptureActivity或CaptureFragment时使用)

    4、继承CameraScan自己实现一个,可参照默认实现类DefaultCameraScan,其它步骤同方式3。(扩展高级用法,谨慎使用)

    其他

    需使用JDK8+编译,在你项目中的build.gradle的android{}中添加配置:

    compileOptions {
    targetCompatibility JavaVersion.VERSION_1_8
    sourceCompatibility JavaVersion.VERSION_1_8
    }

    更多使用详情,请查看app中的源码使用示例或直接查看API帮助文档

    代码下载:ZXingLite.zip

    收起阅读 »

    Android一个专注于App更新,一键傻瓜式集成App版本升级的开源库!

    AppUpdater for Android 是一个专注于App更新,一键傻瓜式集成App版本升级的轻量开源库。(无需担心通知栏适配;无需担心重复点击下载;无需担心App安装等问题;这些AppUpdater都已帮您处理好。) 核心库主要包括app-update...
    继续阅读 »




    AppUpdater for Android 是一个专注于App更新,一键傻瓜式集成App版本升级的轻量开源库。(无需担心通知栏适配;无需担心重复点击下载;无需担心App安装等问题;这些AppUpdater都已帮您处理好。) 核心库主要包括app-updater和app-dialog。

    下载更新和弹框提示分开,是因为这本来就是两个逻辑。完全独立开来能有效的解耦。

    • app-updater 主要负责后台下载更新App,无需担心下载时各种配置相关的细节,一键傻瓜式升级。
    • app-dialog 主要是提供常用的Dialog和DialogFragment,简化弹框提示,布局样式支持自定义。

    app-updater + app-dialog 配合使用,谁用谁知道。

    功能介绍

    •  专注于App更新一键傻瓜式升级
    •  够轻量,体积小
    •  支持监听下载过程
    •  支持下载失败,重新下载
    •  支持下载优先取本地缓存
    •  支持通知栏提示内容和过程全部可配置
    •  支持Android Q(10)
    •  支持取消下载
    •  支持使用OkHttpClient下载

    Gif 展示

    Image

    引入

    Maven:

        //app-updater
    <dependency>
    <groupId>com.king.app</groupId>
    <artifactId>app-updater</artifactId>
    <version>1.0.10</version>
    <type>pom</type>
    </dependency>

    //app-dialog
    <dependency>
    <groupId>com.king.app</groupId>
    <artifactId>app-dialog</artifactId>
    <version>1.0.10</version>
    <type>pom</type>
    </dependency>

    Gradle:


    //----------AndroidX 版本
    //app-updater
    implementation 'com.king.app:app-updater:1.0.10-androidx'
    //app-dialog
    implementation 'com.king.app:app-dialog:1.0.10-androidx'

    //----------Android Support 版本
    //app-updater
    implementation 'com.king.app:app-updater:1.0.10'
    //app-dialog
    implementation 'com.king.app:app-dialog:1.0.10'

    Lvy:

        //app-updater
    <dependency org='com.king.app' name='app-dialog' rev='1.0.10'>
    <artifact name='$AID' ext='pom'></artifact>
    </dependency>

    //app-dialog
    <dependency org='com.king.app' name='app-dialog' rev='1.0.10'>
    <artifact name='$AID' ext='pom'></artifact>
    </dependency>
    如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)
        allprojects {
    repositories {
    //...
    maven { url 'https://dl.bintray.com/jenly/maven' }
    }
    }

    示例

        //一句代码,傻瓜式更新
    new AppUpdater(getContext(),url).start();
        //简单弹框升级
    AppDialogConfig config = new AppDialogConfig(context);
    config.setTitle("简单弹框升级")
    .setOk("升级")
    .setContent("1、新增某某功能、\n2、修改某某问题、\n3、优化某某BUG、")
    .setOnClickOk(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    new AppUpdater.Builder()
    .setUrl(mUrl)
    .build(getContext())
    .start();
    AppDialog.INSTANCE.dismissDialog();
    }
    });
    AppDialog.INSTANCE.showDialog(getContext(),config);
        //简单DialogFragment升级
    AppDialogConfig config = new AppDialogConfig(context);
    config.setTitle("简单DialogFragment升级")
    .setOk("升级")
    .setContent("1、新增某某功能、\n2、修改某某问题、\n3、优化某某BUG、")
    .setOnClickOk(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    new AppUpdater.Builder()
    .setUrl(mUrl)
    .setFilename("AppUpdater.apk")
    .build(getContext())
    .setHttpManager(OkHttpManager.getInstance())//不设置HttpManager时,默认使用HttpsURLConnection下载,如果使用OkHttpClient实现下载,需依赖okhttp库
    .start();
    AppDialog.INSTANCE.dismissDialogFragment(getSupportFragmentManager());
    }
    });
    AppDialog.INSTANCE.showDialogFragment(getSupportFragmentManager(),config);

    更多使用详情,请查看app中的源码使用示例或直接查看API帮助文档

    混淆

    app-updater Proguard rules

    app-dialog Proguard rules

    代码下载:AppUpdater.zip

    收起阅读 »

    一个支持可拖动多边形,可拖动多边形的角改变其形状的任意多边形控件

    DragPolygonViewDragPolygonView for Android 是一个支持可拖动多边形,支持通过拖拽多边形的角改变其形状的任意多边形控件。特性说明 支持添加多个任意多边形 支持通过触摸多边形拖动改变其位置 支...
    继续阅读 »


    DragPolygonView

    DragPolygonView for Android 是一个支持可拖动多边形,支持通过拖拽多边形的角改变其形状的任意多边形控件。

    特性说明

    •  支持添加多个任意多边形
    •  支持通过触摸多边形拖动改变其位置
    •  支持通过触摸多边形的角改变其形状
    •  支持点击、长按、改变等事件监听
    •  支持多边形单选或多选模式

    Gif 展示

    Image

    DragPolygonView 自定义属性说明

    属性值类型默认值说明
    dpvStrokeWidthfloat4画笔描边的宽度
    dpvPointStrokeWidthMultiplierfloat1.0绘制多边形点坐标时基于画笔描边的宽度倍数
    dpvPointNormalColorcolor#FFE5574C多边形点的颜色
    dpvPointPressedColorcolor多边形点按下状态时的颜色
    dpvPointSelectedColorcolor多边形点选中状态时的颜色
    dpvLineNormalColorcolor#FFE5574C多边形边线的颜色
    dpvLinePressedColorcolor多边形边线按下状态的颜色
    dpvLineSelectedColorcolor多边形边线选中状态的颜色
    dpvFillNormalColorcolor#3FE5574C多边形填充的颜色
    dpvFillPressedColorcolor#7FE5574C多边形填充按下状态时的颜色
    dpvFillSelectedColorcolor#AFE5574C多边形填充选中状态时的颜色
    dpvAllowableOffsetsdimension16dp触点允许的误差偏移量
    dpvDragEnabledbooleantrue是否启用拖动多边形
    dpvChangeAngleEnabledbooleantrue是否启用多边形的各个角的角度支持可变
    dpvMultipleSelectionbooleanfalse是否是多选模式,默认:单选模式
    dpvClickToggleSelectedbooleanfalse是否点击就切换多边形的选中状态
    dpvAllowDragOutViewbooleanfalse是否允许多边形拖出视图范围
    dpvTextSizedimension16sp是否允许多边形拖出视图范围
    dpvTextNormalColorcolor#FFE5574C多边形文本的颜色
    dpvTextPressedColorcolor多边形文本按下状态的颜色
    dpvTextSelectedColorcolor多边形文本选中状态的颜色
    dpvShowTextbooleantrue是否显示多边形的文本
    dpvFakeBoldTextbooleanfalse多边形Text的字体是否为粗体

    引入

    Maven:

    <dependency>
    <groupId>com.king.view</groupId>
    <artifactId>dragpolygonview</artifactId>
    <version>1.0.2</version>
    <type>pom</type>
    </dependency>

    Gradle:

    implementation 'com.king.view:dragpolygonview:1.0.2'

    Lvy:

    <dependency org='com.king.view' name='dragpolygonview' rev='1.0.2'>
    <artifact name='$AID' ext='pom'></artifact>
    </dependency>
    如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)
    allprojects {
    repositories {
    maven { url 'https://dl.bintray.com/jenly/maven' }
    }
    }

    示例

    布局示例

        <com.king.view.dragpolygonview.DragPolygonView
    android:id="@+id/dragPolygonView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

    代码示例

        //添加多边形
    dragPolygonView.addPolygon(Polygon polygon);
    //添加多边形(多边形的各个点)
    dragPolygonView.addPolygon(PointF... points);
    //根据位置将多边形改为选中状态
    dragPolygonView.setPolygonSelected(int position);
    //改变监听
    dragPolygonView.setOnChangeListener(OnChangeListener listener);
    //点击监听
    dragPolygonView.setOnPolygonClickListener(OnPolygonClickListener listener);
    //长按监听
    dragPolygonView.setOnPolygonLongClickListener(OnPolygonLongClickListener listener)

    更多使用详情,请查看app中的源码使用示例

    代码下载:jenly1314-DragPolygonView-master.zip

    收起阅读 »

    iOS崩溃统计原理 & 日志分析整理

    简介当应用崩溃时,会产生崩溃日志并且保存在设备上。崩溃日志描述了应用结束时所处的环境信息,通常包含完整的线程堆栈追溯信息,这些数据对于调试应用错误非常有帮助。包含追溯信息的崩溃日志在分析前需要进行符号化。符号化将内存地址替换为更直观的函数名以及行数。崩溃原因崩...
    继续阅读 »

    简介

    当应用崩溃时,会产生崩溃日志并且保存在设备上。崩溃日志描述了应用结束时所处的环境信息,通常包含完整的线程堆栈追溯信息,这些数据对于调试应用错误非常有帮助。
    包含追溯信息的崩溃日志在分析前需要进行符号化。符号化将内存地址替换为更直观的函数名以及行数。

    崩溃原因

    崩溃是指应用产生了系统不允许的行为时,系统终止其运行导致的现象。崩溃发生的原因有:

    1、存在CPU无法运行的代码
    不存在或者无法执行
    2、操作系统执行某项策略,终止程序
    启动时间过长或者消耗过多内存时,操作系统会终止程序运行
    3、编程语言为了避免错误终止程序:抛出异常
    4、开发者为了避免失败终止程序:Assert

    产生崩溃日志

    在程序出现以上问题时,系统会抛出异常,结束程序:
    出现异常情况,终止程序:


    分析崩溃日志

    在发生崩溃时,会产生崩溃日志并且保存在设备上,用于后期对问题定位,崩溃日志的内容包括以下部分:程序信息、异常信息、崩溃堆栈、二进制镜像。下面对每部分进行说明。

    崩溃日志程序信息:

    Incident Identifier: B6FD1E8E-B39F-430B-ADDE-FC3A45ED368C
    CrashReporter Key: f04e68ec62d3c66057628c9ba9839e30d55937dc
    Hardware Model: iPad6,8
    Process: TheElements [303]
    Path: /private/var/containers/Bundle/Application/888C1FA2-3666-4AE2-9E8E-62E2F787DEC1/TheElements.app/TheElements
    Identifier: com.example.apple-samplecode.TheElements
    Version: 1.12
    Code Type: ARM-64 (Native)
    Role: Foreground
    Parent Process: launchd [1]
    Coalition: com.example.apple-samplecode.TheElements [402]

    Date/Time: 2016-08-22 10:43:07.5806 -0700
    Launch Time: 2016-08-22 10:43:01.0293 -0700
    OS Version: iPhone OS 10.0 (14A5345a)
    Report Version: 104

    汇总部分包含崩溃发生环境的基本信息:

    1、Incident Identifier:日志ID。

    2、CrashReport Key:设备匿名ID,同一设备的崩溃日志该值相同。

    3、Beta Identifier:设备和崩溃应用组合ID。

    4、Process:执行程序名,等同CFBundleExecutable。

    5、Version:程序版本号,等同CFBundleVersion/CFBundleVersionString。

    6、Code type:程序构造:ARM-64、ARM、x86

    异常信息:

    Exception Type: EXC_BAD_ACCESS (SIGSEGV)
    Exception Subtype: KERN_INVALID_ADDRESS at 0x0000000000000000
    Termination Signal: Segmentation fault: 11
    Termination Reason: Namespace SIGNAL, Code 0xb
    Terminating Process: exc handler [0]
    Triggered by Thread: 0

    异常信息:

    1、Exception Codes:使用十六进制表示的程序特定信息,一般不展示。

    2、Exception Subtype:易读(相比十六进制地址)的异常信息。

    3、Exception Message:异常的额外信息。

    4、Exception Note:不特指某种异常类型的额外信息。

    5、Termination Reason:程序终止的异常信息。

    6、Triggered Thread:异常发生时的线程。

    崩溃堆栈:

    Thread 0 name: Dispatch queue: com.apple.main-thread
    Thread 0 Crashed:
    0 TheElements 0x000000010006bc20 -[AtomicElementViewController myTransitionDidStop:finished:context:] (AtomicElementViewController.m:203)
    1 UIKit 0x0000000194cef0f0 -[UIViewAnimationState sendDelegateAnimationDidStop:finished:] + 312
    2 UIKit 0x0000000194ceef30 -[UIViewAnimationState animationDidStop:finished:] + 160
    3 QuartzCore 0x0000000192178404 CA::Layer::run_animation_callbacks(void*) + 260
    4 libdispatch.dylib 0x000000018dd6d1c0 _dispatch_client_callout + 16
    5 libdispatch.dylib 0x000000018dd71d6c _dispatch_main_queue_callback_4CF + 1000
    6 CoreFoundation 0x000000018ee91f2c __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 12
    7 CoreFoundation 0x000000018ee8fb18 __CFRunLoopRun + 1660
    8 CoreFoundation 0x000000018edbe048 CFRunLoopRunSpecific + 444
    9 GraphicsServices 0x000000019083f198 GSEventRunModal + 180
    10 UIKit 0x0000000194d21bd0 -[UIApplication _run] + 684
    11 UIKit 0x0000000194d1c908 UIApplicationMain + 208
    12 TheElements 0x00000001000653c0 main (main.m:55)
    13 libdyld.dylib 0x000000018dda05b8 start + 4

    Thread 1:
    0 libsystem_kernel.dylib 0x000000018deb2a88 __workq_kernreturn + 8
    1 libsystem_pthread.dylib 0x000000018df75188 _pthread_wqthread + 968
    2 libsystem_pthread.dylib 0x000000018df74db4 start_wqthread + 4

    ...

    第一行列出了线程信息以及所在队列,之后是追溯链中独立栈帧的详细信息:

    1、栈帧号。栈帧号为0的代表当前执行停顿的函数,1则是调用当前停顿函数的主调函数,即0为1的被调函数,1为0的主调函数,以此类推。
    2、执行函数所在的二进制包
    3、地址信息:对于0栈帧来说,代表当前执行停顿的地址。其他栈帧则是获取控制权后接下来执行的地址。
    4、函数名

    二进制镜像:

    Binary Images:
    0x100060000 - 0x100073fff TheElements arm64 <2defdbea0c873a52afa458cf14cd169e> /var/containers/Bundle/Application/888C1FA2-3666-4AE2-9E8E-62E2F787DEC1/TheElements.app/TheElements
    ...

    日之内包含多个二进制镜像,每个二进制镜像内包含以下信息:

    1、二进制镜像在程序内的地址空间
    2、二进制的名称或者bundleID
    3、二进制镜像的架构信息 arm64等
    4、二进制镜像的UUID,每次构建都会改变,该值用于在符号化日志时定位对应的dSYM文件。
    5、磁盘上的二进制路径

    符号化

    app.xcarchive文件,包内容包含dSYM和应用的二进制文件。
    更精确的符号化,可以结合崩溃日志、项目二进制文件、dSYM文件,对其进行反汇编,从而获得更详细的信息。

    符号化就是将追溯的地址信息转换成函数名及行数等信息,便于研发人员定位问题。
    当程序结束运行时,会产生崩溃日志,日志内包含每个线程的堆栈信息。当我们使用Xcode进行调试时,崩溃或者断点信息都会展示出实例和方法名等信息(符号信息)。相反,当应用被发布后,符号信息并不会包含在应用的二进制文件中,所以服务端收到的是未符号化的包含十六进制地址信息的日志文件。
    查看本机崩溃日志步骤如下:

    1、将手机连接到Mac
    2、启动Xcode->Window->Devices and simulators
    3、选择View Device Logs

    选择左侧应用,之后就可以在右侧看到崩溃日志信息:


    日志内包含符号化内容-[__NSArrayI objectAtIndex:]和十六进制地址0x000db142 0xb1000 + 172354。这种日志类型成为部分符号化崩溃日志。
    部分符号化的原因在于,Xcode只能符号化系统组件,例如UIKit、CoreFoundation等。但是对于非系统库产生的崩溃,在没有符号表的情况下就无法符号化。
    分析第三行未符号化的代码:

    0x000db142 0xb1000 + 172354

    以上内容说明了崩溃发生在内存地址0x000db142,此地址和0xb1000 + 172354是相等的。0xb1000代表这部分许的起始地址,172354代表偏移位。

    崩溃日志类型:

    崩溃日志可能包含几种状态:未符号化、完全符号化、部分符号化。
    未符号化的崩溃日志追溯链中没有函数的名字等信息,而是二进制镜像执行代码的十六进制地址。
    完全符号化的崩溃日志中,所有的十六进制地址都被替换为对应的函数符号。

    符号化流程

    符号化需要两部分内容:崩溃的二进制代码和编译产生的对应dSYM。

    符号表

    当编译器将源码转换为机器码时,会生成一个调试符号表,表内是二进制结构到原始源码的映射关系。调试符号表保存在dSYM(debug symbol调试符号表)文件内。调试模式下符号表会保存在编译的二进制内,发布模式则将符号表保存在dSYM文件内用于减少包的体积。

    当崩溃发生时,会在设备存储一份未符号化的崩溃日志
    获取崩溃日志后,通过dSYM对追溯链中的每个地址进行符号化,转换为函数信息,产生的结果就是符号化后的崩溃日志。

    函数调用堆栈

    我们知道,崩溃日志内包含函数调用的追溯信息,明白堆栈是怎么产生的有利于我们理解和分析崩溃日志。


    函数调用是在栈进行的,函数从调用和被调用方分为:主调函数和被调函数,这次我们只讨论每个函数在栈中的几个核心部分:

    1、上一个函数(主调函数)的堆栈信息。
    2、入参。
    3、局部变量。

    入参和局部变量容易理解,下面讨论为什么要保存主调函数的堆栈信息。
    说到这点就需要聊到寄存器。

    寄存器


    寄存器的类型和基本功能:

    eax:累加寄存器,用于运算。
    ebx:基址寄存器,用于地址索引。
    ecx:计数寄存器,用于计数。
    edx:数据寄存器,用于数据传递。
    esi:源变址寄存器,存放相对于DS段之源变址指针。
    edi:目的变址寄存器,存放相对于ES段之目的的变址指针。
    esp:堆栈指针,指向当前堆栈位置。
    ebp:基址指针寄存器,相对基址位置。

    寄存器约定

    背景:

    1、所有函数都可以访问和操作寄存器,寄存器对于单个CPU来说数量是固定的
    2、单个CPU来说,某一时刻只有一个函数在执行
    3、需要保证函数调用其他函数时,被调函数不会修改或覆盖主调函数稍后使用的寄存器值

    被调函数在执行时,需要使用寄存器来保存数据和执行计算,但是在被调函数完成时,需要把寄存器还原,用于主调函数的执行,所以出现了寄存器约定。

    约定内容:

    1、主调函数的保存寄存器,在唤起被调函数前,需要显示的将其保存在栈中。
    主调寄存器:%eax、%edx、%ecx
    2、被调函数的保存寄存器,使用前压栈,并在函数返回前从栈中恢复原值。
    被调寄存器:%ebx、%esi、%edi
    3、被调函数必须保存%ebp和%esp,并在函数返回后恢复调用前的值。

    遵守寄存器约定的函数堆栈调用

    了解了寄存器功能和寄存器约定后,我们再看函数调用堆栈:


    1、栈帧逻辑:栈帧的边界由栈帧基地址指针EBP和堆栈指针ESP界定(指针存放在相应寄存器中)。EBP指向当前栈帧底部(高地址),在当前栈帧内位置固定;ESP指向当前栈帧顶部(低地址),当程序执行时ESP会随着数据的入栈和出栈而移动。因此函数中对大部分数据的访问都基于EBP进行。
    2、保存栈帧:被调函数必须保持寄存器%ebp和%esp,并在函数返回后将其恢复到调用前的值,亦即必须恢复主调函数的栈帧。
    3、回溯:所以获取到崩溃时线程的ebp和esp 就能回溯到上一个调用,依次类推,回溯出所有的调用堆栈

    总结

    通过以上内容,我们了解了崩溃日志产生原理、崩溃日志内容和崩溃日志分析,下面分享几个分析崩溃日志的小提示作为结束:

    1、不止关注崩溃本行,结合上下文进行分析。
    2、不止关注崩溃线程,要结合其他线程的堆栈信息。
    3、通过多个崩溃日志,组合分析。
    4、使用地址定位和野指针工具重现内存问题。

    参考资料

    Apple Understanding and Analyzing Application Crash Reports
    Overview Of iOS Crash Reporting Tools
    Understanding Crashes and Crash Logs Video

    链接:https://www.jianshu.com/p/e05498960209

    收起阅读 »

    如何构建优雅的ViewController

    前言关于ViewController讨论的最多的是它的肥胖和臃肿,但是哪怕是采用MVC模式,ViewController同样可以写的很优雅,这无关乎设计模式,对于那些以设计模式论高低的,我只能呵呵。其实这关乎的是你对设计模式的理解有多深,你对于职责划分的认知是...
    继续阅读 »

    前言

    关于ViewController讨论的最多的是它的肥胖和臃肿,但是哪怕是采用MVC模式,ViewController同样可以写的很优雅,这无关乎设计模式,对于那些以设计模式论高低的,我只能呵呵。其实这关乎的是你对设计模式的理解有多深,你对于职责划分的认知是否足够清晰。ViewController也从很大程度上反应一个程序员的真实水平,一个平庸的程序员他的ViewController永远是臃肿的、肥胖的,什么功能都可以往里面塞,不同功能间缺乏清晰的界限。而一个优秀的程序员它的ViewController显得如此优雅,让你产生一种竟不能修改一笔一画的感觉。

    ViewController职责

    1、UI 属性 和 布局
    2、用户交互事件
    3、用户交互事件处理和回调

    用户交互事件处理: 通常会交给其他对象去处理

    回调: 可以根据具体的设计模式和应用场景交给 ViewController 或者其他对象处理

    而通常我们在阅读别人ViewController代码的时候,我们关注的是什么?

    控件属性配置在哪里?
    用户交互的入口位置在哪里?
    用户交互会产生什么样的结果?(回调在哪里?)
    所以从这个角度来说,这三个功能一开始就应该是被分离的,需要有清新明确的界限。因为谁都不希望自己在查找交互入口的时候 ,去阅读一堆控件冗长的控件配置代码, 更不愿意在一堆代码去慢慢理清整个用户交互的流程。 我们通常只关心我当前最关注的东西,当看到一堆无关的代码时,第一反应就是我想注释掉它。

    基于协议分离UI属性的配置

    protocol MFViewConfigurer {
    var rootView: UIView { get }
    var contentViews: [UIView] { get }
    var contentViewsSettings: [() -> Void] { get }

    func addSubViews()
    func configureSubViewsProperty()
    func configureSubViewsLayouts()

    func initUI()
    }

    依赖这个协议就可以完成所有控件属性配置,然后通过extension protocol 大大减少重复代码,同时提高可读性

    extension MFViewConfigurer {
    func addSubViews() {
    for element in contentViews {
    if let rootView = rootView as? UIStackView {
    rootView.addArrangedSubview(element)
    } else {
    rootView.addSubview(element)
    }
    }
    }

    func configureSubViewsProperty() {
    for element in contentViewsSettings {
    element()
    }
    }

    func configureSubViewsLayouts() {
    }

    func initUI() {
    addSubViews()
    configureSubViewsProperty()
    configureSubViewsLayouts()
    }
    }

    这里 我将控件的添加和控件的配置分成两个函数addSubViews和configureSubViewsProperty, 因为在我的眼里函数就应该遵循单一职责这个概念:
    addSubViews: 明确告诉阅读者,我这个控制器只有这些控件
    configureSubViewsProperty: 明确告诉阅读者,控件的所有属性配置都在这里,想要修改属性请阅读这个函数

    来看一个实例:

    override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.

    // 初始化 UI
    initUI()

    // 绑定用户交互事件
    bindEvent()

    // 将ViewModel.value 绑定至控件
    bindValueToUI()

    }

    // MARK: - UI configure

    // MARK: - UI

    extension MFWeatherViewController: MFViewConfigurer {
    var contentViews: [UIView] { return [scrollView, cancelButton] }

    var contentViewsSettings: [() -> Void] {
    return [{
    self.view.backgroundColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.7)
    self.scrollView.hiddenSubViews(isHidden: false)
    }]
    }

    func configureSubViewsLayouts() {
    cancelButton.snp.makeConstraints { make in
    if #available(iOS 11, *) {
    make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top)
    } else {
    make.top.equalTo(self.view.snp.top).offset(20)
    }

    make.left.equalTo(self.view).offset(20)
    make.height.width.equalTo(30)
    }

    scrollView.snp.makeConstraints { make in
    make.top.bottom.left.right.equalTo(self.view)
    }
    }

    }


    而对于UIView 这套协议同样适用

    ```Swift
    // MFWeatherSummaryView
    private override init(frame: CGRect) {
    super.init(frame: frame)

    initUI()
    }


    // MARK: - UI

    extension MFWeatherSummaryView: MFViewConfigurer {
    var rootView: UIView { return self }

    var contentViews: [UIView] {
    return [
    cityLabel,
    weatherSummaryLabel,
    temperatureLabel,
    weatherSummaryImageView,
    ]
    }

    var contentViewsSettings: [() -> Void] {
    return [UIConfigure]
    }

    private func UIConfigure() {
    backgroundColor = UIColor.clear
    }

    public func configureSubViewsLayouts() {
    cityLabel.snp.makeConstraints { make in
    make.top.centerX.equalTo(self)
    make.bottom.equalTo(temperatureLabel.snp.top).offset(-10)
    }

    temperatureLabel.snp.makeConstraints { make in
    make.top.equalTo(cityLabel.snp.bottom).offset(10)
    make.right.equalTo(self.snp.centerX).offset(0)
    make.bottom.equalTo(self)
    }

    weatherSummaryImageView.snp.makeConstraints { make in
    make.left.equalTo(self.snp.centerX).offset(20)
    make.bottom.equalTo(temperatureLabel.snp.lastBaseline)
    make.top.equalTo(weatherSummaryLabel.snp.bottom).offset(5)
    make.height.equalTo(weatherSummaryImageView.snp.width).multipliedBy(61.0 / 69.0)
    }

    weatherSummaryLabel.snp.makeConstraints { make in
    make.top.equalTo(temperatureLabel).offset(20)
    make.centerX.equalTo(weatherSummaryImageView)
    make.bottom.equalTo(weatherSummaryImageView.snp.top).offset(-5)
    }
    }
    }

    由于我使用的是MVVM模式,所以viewDidLoad 和MVC模式还是有些区别,如果是MVC可能就是这样

    override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.

    // 初始化 UI
    initUI()

    // 用户交互事件入口
    addEvents()


    }

    // MARK: callBack
    ......

    由于MVC的回调模式很难统一,有Delegate, closure, notification 等等,所以回调通常会散落在控制器各个角落。最好加个MARK flag, 尽量收集在同一个区域中, 同时对于每个回调加上必要的注释:

    1、由哪种操作触发
    2、会导致什么后果
    3、最终会留下哪里

    所以从这个角度来说UITableViewDataSource 和 UITableViewDelegate 完全是两种不一样的行为, 一个是 configure UI , 一个是 control behavior , 所以不要在把这两个东西写一块了, 真的很难看。

    总结

    基于职责对代码进行分割,这样会让你的代码变得更加优雅简洁,会大大减少一些万金油代码的出现,减少阅读代码的成本也是我们优化的一个方向,比较谁都不想因为混乱的代码影响自己的心情

    链接:https://www.jianshu.com/p/266cbca1439c

    收起阅读 »

    【面试专题】Android屏幕刷新机制

    这个问题在其他人整理的面试宝典中也有提及,一般来说都是问View的刷新,基本上从ViewRootImpl的scheduleTraversals()方法开始讲就可以了。之前看别人面试斗鱼的面经,被问到了Android屏幕刷新机制、双缓冲、三缓冲、黄油计划,然后我...
    继续阅读 »

    这个问题在其他人整理的面试宝典中也有提及,一般来说都是问View的刷新,基本上从ViewRootImpl的scheduleTraversals()方法开始讲就可以了。之前看别人面试斗鱼的面经,被问到了Android屏幕刷新机制、双缓冲、三缓冲、黄油计划,然后我面网易云的时候也确实被问到了这个题目。


    屏幕刷新这一整套,你把我这篇文章里的内容讲清楚了,肯定ok了。网易云还附加问了我CPU和GPU怎么交换绘制数据的,这个我个人认为完全是加分题了,我答不出来,感兴趣的小伙伴可以去看一看,你要是能说清楚,肯定能让面试官眼前一亮。


    双缓冲


    在讲双缓冲这个概念之前,先来了解一些基础知识。


    显示系统基础


    在一个典型的显示系统中,一般包括CPU、GPU、Display三个部分, CPU负责计算帧数据,把计算好的数据交给GPU, GPU会对图形数据进行渲染,渲染好后放到buffer(图像缓冲区)里存起来,然后Display(屏幕或显示器)负责把buffer里的数 据呈现到屏幕上。



    • 画面撕裂


    屏幕刷新频是固定的,比如每16.6ms从buffer取数据显示完一帧,理想情况下帧率和刷新频率保持一致,即每绘制完成一 帧,显示器显示一帧。但是CPU/GPU写数据是不可控的,所以会出现buffer里有些数据根本没显示出来就被重写了,即 buffer里的数据可能是来自不同的帧的。当屏幕刷新时,此时它并不知道buffer的状态,因此从buffer抓取的帧并不是完整的一帧画面,即出现画面撕裂。


    简单说就是Display在显示的过程中,buffer内数据被CPU/GPU修改,导致画面撕裂。


    那咋解决画面撕裂呢? 答案是使用双缓冲。


    双缓冲


    由于图像绘制和屏幕读取 使用的是同个buffer,所以屏幕刷新时可能读取到的是不完整的一帧画面。


    双缓冲,让绘制和显示器拥有各自的buffer:GPU 始终将完成的一帧图像数据写入到 Back Buffer,而显示器使用 Frame Buffer,当屏幕刷新时,Frame Buffer 并不会发生变化,当Back buffer准备就绪后,它们才进行交换。


    VSync


    什么时候进行两个buffer的交换呢?


    假如是 Back buffer准备完成一帧数据以后就进行,那么如果此时屏幕还没有完整显示上一帧内容的话,肯定是会出问题的。 看来只能是等到屏幕处理完一帧数据后,才可以执行这一操作了。


    当扫描完一个屏幕后,设备需要重新回到第一行以进入下一次的循环,此时有一段时间空隙,称为VerticalBlanking Interval(VBI)。这个时间点就是我们进行缓冲区交换的最佳时间。因为此时屏幕没有在刷新,也就避免了交换过程中出现画面撕裂的状况。


    VSync(垂直同步)是VerticalSynchronization的简写,它利用VBI时期出现的vertical sync pulse(垂直同步脉冲)来保证双缓冲在最佳时间点才进行交换。另外,交换是指各自的内存地址,可以认为该操作是瞬间完成。


    所以说VSync这个概念并不是Google首创的,它在早年的PC机领域就已经出现了。


    Android屏幕刷新机制


    先总体概括一下,Android屏幕刷新使用的是“双缓存+VSync机制”,单纯的双缓冲模式容易造成jank(丢帧)现象,为了解决这个问题,Google在 Android4.1 提出了Project Butter(?油工程),引入了 drawing with VSync 的概念。


    jank(丢帧)


    VSync.jpeg


    以时间的顺序来看下将会发生的过程:



    1. Display显示第0帧数据,此时CPU和GPU渲染第1帧画面,且在Display显示下一帧前完成

    2. 因为渲染及时,Display在第0帧显示完成后,也就是第1个VSync后,缓存进行交换,然后正常显示第1帧

    3. 接着第2帧开始处理,是直到第2个VSync快来前才开始处理的。

    4. 第2个VSync来时,由于第2帧数据还没有准备就绪,缓存没有交换,显示的还是第1帧。这种情况被Android开发组命名为“Jank”,即发生了丢帧。

    5. 当第2帧数据准备完成后,它并不会?上被显示,而是要等待下一个VSync 进行缓存交换再显示。


    所以总的来说,就是屏幕平白无故地多显示了一次第1帧。 原因是第2帧的CPU/GPU计算 没能在VSync信号到来前完成。


    这里注意一下一个细节,jank(丢帧、掉帧),不是说这一帧丢弃了不显示,而是这一帧延迟显示了,因为缓存交换的时机只能等下一个VSync了。


    黄油计划 —— drawing with VSync


    为了优化显示性能,Google在Android 4.1系统中对Android Display系统进行了重构,实现了Project Butter(?油工程): 系统在收到VSync pulse后,将?上开始下一帧的渲染。即一旦收到VSync通知(16ms触发一次),CPU和GPU 才立刻开 始计算然后把数据写入buffer。如下图:


    VSync2.jpeg


    CPU/GPU根据VSYNC信号同步处理数据,可以让CPU/GPU有完整的16ms时间来处理数据,减少了jank。 一句话总结,VSync同步使得CPU/GPU充分利用了16.6ms时间,减少jank。


    问题又来了,如果界面比较复杂,CPU/GPU的处理时间较?,超过了16.6ms呢?如下图:


    VSync3.jpeg



    1. 在第二个时间段内,但却因 GPU 还在处理 B 帧,缓存没能交换,导致 A 帧被重复显示。

    2. 而B完成后,又因为缺乏VSync pulse信号,它只能等待下一个signal的来临。于是在这一过程中,有一大段时间是被浪费的。

    3. 当下一个VSync出现时,CPU/GPU?上执行操作(A帧),且缓存交换,相应的显示屏对应的就是B。这时看起来就是正常的。只不过由于执行时间仍然超过16ms,导致下一次应该执行的缓冲区交换又被推迟了——如此循环反复,便出现了越来越多的“Jank”。


    为什么 CPU 不能在第二个 16ms 处理绘制工作呢? 因为只有两个 buffer,Back buffer正在被GPU用来处理B帧的数据, Frame buffer的内容用于Display的显示,这样两个 buffer都被占用,CPU 则无法准备下一帧的数据。 那么,如果再提供一个buffer,CPU、GPU 和显示设备都能使用各自的 buffer工作,互不影响。这就是三缓冲的来源了。


    三缓冲


    三缓存就是在双缓冲机制基础上增加了一个 Graphic Buffer 缓冲区,这样可以最大限度的利用空闲时间,带来的坏处是多使用的一个 Graphic Buffer 所占用的内存。


    VSync4.jpeg



    1. 第一个Jank,是不可避免的。但是在第二个 16ms 时间段,CPU/GPU 使用 第三个 Buffer 完成C帧的计算,虽然还是 会多显示一次 A 帧,但后续显示就比较顺畅了,有效避免 Jank 的进一步加剧。

    2. 注意在第3段中,A帧的计算已完成,但是在第4个vsync来的时候才显示,如果是双缓冲,那在第三个vynsc就可以显示了。


    三缓冲有效利用了等待VSync的时间,减少了jank,但是带来了延迟。是不是 Buffer 越多越好呢?这个是否定的, Buffer 正常还是两个,当出现 Jank 后三个足以。


    Choreographer


    上边讲的都是基础的刷新知识,那么在 Android 系统中,真正来实现绘制的类叫Choreographer


    Choreographer负责对CPU/GPU绘制的指导 —— 收到VSync信号才开始绘制,保证绘制拥有完整 16.6ms,避免绘制的随机性。


    通常 应用层不会直接使用Choreographer,而是使用更高级的API,例如动画和View绘制相关的 ValueAnimator.start()、View.invalidate()等。


    (这边补充说一个面试题,属性动画更新时会回调onDraw吗?不会,因为它内部是通过AnimationHandler中的Choreographer机制来实现的更新,具体的逻辑,如果以后有时间的话可以写篇文章来说一说。)


    业界一般通过Choreographer来监控应用的帧率。


    (这个东西也是个面试题,会问你如何检测应用的帧率?你可以提一下Choreographer里面的FrameCallback,然后结合一些第三方库的实现具体说一下。)


    View刷新的入口


    Activity启动,走完onResume方法后,会进行window的添加。window添加过程会调用ViewRootImpl的setView()方法, setView()方法会调用requestLayout()方法来请求绘制布局,requestLayout()方法内部又会走到scheduleTraversals()方法。最后会走到performTraversals()方法,接着到了我们熟知的测量、布局、绘制三大流程了。


    当我们使用 ValueAnimator.start()、View.invalidate()时,最后也是走到ViewRootImpl的 scheduleTraversals()方法。(View.invalidate()内部会循环获取ViewParent直到ViewRootImpl的invalidateChildInParent()方法,然后走到scheduleTraversals(),可自行查看源码)


    即所有UI的变化都是走到ViewRootImpl的scheduleTraversals()方法。


    这里注意一个点:scheduleTraversals()之后不是立即就执行performTraversals()的,它们中间隔了一个Choreographer机制。简单来说就是scheduleTraversals()中,Choreographer会去请求native的VSync信号,VSync信号来了之后才会去调用performTraversals()方法进行View绘制的三大流程。



    //ViewRootImpl.java
    void scheduleTraversals() {
    if (!mTraversalScheduled) {
    mTraversalScheduled = true;
    //添加同步屏障,屏蔽同步消息,保证VSync到来立即执行绘制
    mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
    //mTraversalRunnable是TraversalRunnable实例,最终走到run(),也即doTraversal();
    mChoreographer.postCallback(
    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
    if (!mUnbufferedInputDispatch) {
    scheduleConsumeBatchedInput();
    }
    notifyRendererOfFramePending();
    pokeDrawLockIfNeeded();
    }
    }
    final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
    doTraversal();
    }
    }
    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
    void doTraversal() {
    if (mTraversalScheduled) {
    mTraversalScheduled = false;
    //移除同步屏障
    mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); ...
    //开始三大绘制流程
    performTraversals();
    ...
    }
    }


    1. postSyncBarrier 开启同步屏障,保证VSync到来后立即执行绘制

    2. mChoreographer.postCallback()方法,发送一个会在下一帧执行的回调,即在下一个VSync到来时会执行 TraversalRunnable–>doTraversal()—>performTraversals()–>绘制流程。


    Choreographer


    初始化


    mChoreographer,是在ViewRootImpl的构造方法内使用 Choreographer.getInstance()创建。


    Choreographer和Looper一样是线程单例的,通过ThreadLocal机制来保证唯一性。因为Choreographer内部通过FrameHandler来发送消息,所以初始化的时候会先判断当前线程有无Looper,没有的话直接抛异常。


    public static Choreographer getInstance() {
    return sThreadInstance.get();
    }

    private static final ThreadLocal<Choreographer> sThreadInstance =
    new ThreadLocal<Choreographer>() {
    @Override
    protected Choreographer initialValue() {
    Looper looper = Looper.myLooper();
    if (looper == null) {
    //当前线程要有looper,Choreographer实例需要传入
    throw new IllegalStateException("The current thread must have a looper!");
    }
    Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
    if (looper == Looper.getMainLooper()) {
    mMainInstance = choreographer;
    }
    return choreographer;
    }
    };

    postCallback


    mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null)方法,第一个参数是CALLBACK_TRAVERSAL,表示回调任务的类型,共有以下5种类型:


    //输入事件,首先执行
    public static final int CALLBACK_INPUT = 0;
    //动画,第二执行
    public static final int CALLBACK_ANIMATION = 1;
    //插入更新的动画,第三执行
    public static final int CALLBACK_INSETS_ANIMATION = 2;
    //绘制,第四执行
    public static final int CALLBACK_TRAVERSAL = 3;
    //提交,最后执行,
    public static final int CALLBACK_COMMIT = 4;

    五种类型任务对应存入对应的CallbackQueue中,每当收到 VSYNC 信号时,Choreographer 将首先处理 INPUT 类型的任 务,然后是 ANIMATION 类型,最后才是 TRAVERSAL 类型。


    postCallback()内部调用postCallbackDelayed(),接着又调用postCallbackDelayedInternal(),正常消息执行scheduleFrameLocked,延迟运行的消息会发送一个MSG_DO_SCHEDULE_CALLBACK类型的meessage:


    private void postCallbackDelayedInternal(int callbackType,
    Object action, Object token, long delayMillis)
    {
    ...
    synchronized (mLock) {
    ...
    mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
    if (dueTime <= now) { //立即执行
    scheduleFrameLocked(now);
    } else {
    //延迟运行,最终也会走到scheduleFrameLocked()
    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
    msg.arg1 = callbackType;
    msg.setAsynchronous(true);
    mHandler.sendMessageAtTime(msg, dueTime);
    }
    }
    }

    FrameHandler这个类是内部专门用来处理消息的,可以看到延迟的MSG_DO_SCHEDULE_CALLBACK类型消息最终也是走到scheduleFrameLocked:


    private final class FrameHandler extends Handler {
    public FrameHandler(Looper looper) {
    super(looper);
    }
    @Override
    public void handleMessage(Message msg) {
    switch (msg.what) {
    case MSG_DO_FRAME:
    // 执行doFrame,即绘制过程
    doFrame(System.nanoTime(), 0);
    break;
    case MSG_DO_SCHEDULE_VSYNC:
    //申请VSYNC信号,例如当前需要绘制任务时
    doScheduleVsync();
    break;
    case MSG_DO_SCHEDULE_CALLBACK:
    //需要延迟的任务,最终还是执行上述两个事件
    doScheduleCallback(msg.arg1);
    break;
    }
    }
    }

    void doScheduleCallback(int callbackType) {
    synchronized (mLock) {
    if (!mFrameScheduled) {
    final long now = SystemClock.uptimeMillis();
    if (mCallbackQueues[callbackType].hasDueCallbacksLocked(now)) {
    scheduleFrameLocked(now);
    }
    }
    }
    }

    申请VSync信号


    scheduleFrameLocked()方法里面就会去真正的申请 VSync 信号了。


    private void scheduleFrameLocked(long now) {
    if (!mFrameScheduled) {
    mFrameScheduled = true;
    if (USE_VSYNC) {
    //当前执行的线程,是否是mLooper所在线程
    if (isRunningOnLooperThreadLocked()) {
    //申请 VSYNC 信号
    scheduleVsyncLocked();
    } else {
    // 若不在,就用mHandler发送消息到原线程,最后还是调用scheduleVsyncLocked方法
    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
    msg.setAsynchronous(true);//异步
    mHandler.sendMessageAtFrontOfQueue(msg);
    }
    } else {
    // 如果未开启VSYNC则直接doFrame方法(4.1后默认开启)
    final long nextFrameTime = Math.max(
    mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
    Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
    msg.setAsynchronous(true);//异步
    mHandler.sendMessageAtTime(msg, nextFrameTime);
    }
    }
    }

    VSync信号的注册和监听是通过mDisplayEventReceiver实现的。mDisplayEventReceiver是在Choreographer的构造方法中创建的,是FrameDisplayEventReceiver的实例。 FrameDisplayEventReceiver是 DisplayEventReceiver 的子类,


    private void scheduleVsyncLocked() {
    mDisplayEventReceiver.scheduleVsync();
    }

    public DisplayEventReceiver(Looper looper, int vsyncSource) {
    if (looper == null) {
    throw new IllegalArgumentException("looper must not be null");
    }
    mMessageQueue = looper.getQueue();
    // 注册native的VSYNC信号监听者
    mReceiverPtr = nativeInit(new WeakReference<DisplayEventReceiver>(this), mMessageQueue,vsyncSource);
    mCloseGuard.open("dispose");
    }

    VSync信号回调


    native的VSync信号到来时,会走到onVsync()回调:


    private final class FrameDisplayEventReceiver extends DisplayEventReceiver
    implements Runnable
    {

    @Override
    public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
    ...
    //将本身作为runnable传入msg, 发消息后 会走run(),即doFrame(),也是异步消息
    Message msg = Message.obtain(mHandler, this);
    msg.setAsynchronous(true);
    mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
    }

    @Override
    public void run() {
    mHavePendingVsync = false;
    doFrame(mTimestampNanos, mFrame);
    }
    }

    (这里补充一个面试题:页面UI没有刷新的时候onVsync()回调也会执行吗?不会,因为VSync是UI需要刷新的时候主动去申请的,而不是native层不停地往上面去推这个回调的,这边要注意。)


    doFrame


    doFrame()方法中会通过doCallbacks()方法去执行各种callbacks,主要内容就是取对应任务类型的队列,遍历队列执行所有任务,其中就包括了 ViewRootImpl 发起的绘制任务mTraversalRunnable了。mTraversalRunnable执行doTraversal()方法,移除同步屏障,调用performTraversals()开始三大绘制流程。


    到这里整个流程就闭环了。


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

    java设计模式:备忘录模式

    前言 备忘录模式能记录一个对象的内部状态,当用户后悔时能撤销当前操作,使数据恢复到它原先的状态。 定义 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。该模式又叫快照模式。 ...
    继续阅读 »


    前言


    备忘录模式能记录一个对象的内部状态,当用户后悔时能撤销当前操作,使数据恢复到它原先的状态。


    定义


    在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。该模式又叫快照模式。
    在这里插入图片描述


    优点


    提供了一种可以恢复状态的机制。当用户需要时能够比较方便地将数据恢复到某个历史的状态。
    实现了内部状态的封装。除了创建它的发起人之外,其他对象都不能够访问这些状态信息。
    简化了发起人类。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则。


    缺点


    资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。


    结构



    • 发起人(Originator)角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。

    • 备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。

    • 管理者(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。


    实现


    生活中最常用的计算器自己拥有备忘录的功能,用户计算完后软件会自动为用户记录最后几次的计算结果,我们可以模拟用户使用计算器的过程,以及打开备忘录查看记录。


    package com.rabbit;

    /**
    * 备忘录发起人,模拟计算器加法运算
    * Created by HASEE on 2018/4/29.
    */
    public class Originator {

    private double num1;

    private double num2;

    //创建备忘录对象
    public Memento createMemento() {
    return new Memento(num1, num2);
    }

    public Originator(double num1, double num2) {
    this.num1 = num1;
    this.num2 = num2;
    System.out.println(num1 + " + " + num2 + " = " + (num1 + num2));
    }

    }
    package com.rabbit;

    /**
    * 备忘录,要保存的属性
    * Created by HASEE on 2018/4/29.
    */
    public class Memento {

    private double num1;//计算器第一个数字

    private double num2;//计算器第二个数字

    private double result;//计算结果

    public Memento(double num1, double num2) {
    this.num1 = num1;
    this.num2 = num2;
    this.result = num1 + num2;
    }

    public void show() {
    System.out.println(num1 + " + " + num2 + " = " + result);
    }

    }
    package com.rabbit;

    import java.util.ArrayList;
    import java.util.List;

    /**
    * 备忘录管理者
    * Created by HASEE on 2018/4/29.
    */
    public class Caretaker {

    private List<Memento> mementos;

    public boolean addMenento(Memento memento) {
    if (mementos == null) {
    mementos = new ArrayList<>();
    }
    return mementos.add(memento);
    }

    public List<Memento> getMementos() {
    return mementos;
    }

    public static Caretaker newInstance() {
    return new Caretaker();
    }
    }
    package com.rabbit;

    import org.junit.Test;

    import java.util.Random;

    /**
    * Created by HASEE on 2018/4/29.
    */
    public class Demo {

    @Test
    public void test() {
    Caretaker c = Caretaker.newInstance();
    //使用循环模拟用户使用计算器做加法运算
    Random ran = new Random(1000);
    for (int i = 0; i < 5; i++) {
    //用户计算
    Originator o = new Originator(ran.nextDouble(), ran.nextDouble());
    //计算器软件将用户的计算做备份,以便可以查看历史
    c.addMenento(o.createMemento());
    }
    System.out.println("---------------------用户浏览历史记录---------------------");
    for (Memento m : c.getMementos()) {
    m.show();
    }
    System.out.println("---------------------用户选择一条记录查看----------------------");
    c.getMementos().get(2).show();
    }

    }

    收起阅读 »

    java设计模式:访问者模式

    前言 访问者模式是一种将数据操作和数据结构分离的设计模式。 定义 将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。它将对数据的操作与数据结构...
    继续阅读 »


    前言


    访问者模式是一种将数据操作和数据结构分离的设计模式。


    定义


    将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。它将对数据的操作与数据结构进行分离,是行为类模式中最复杂的一种模式。


    优点



    1. 扩展性好。能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。

    2. 复用性好。可以通过访问者来定义整个对象结构通用的功能,从而提高系统的复用程度。

    3. 灵活性好。访问者模式将数据结构与作用于结构上的操作解耦,使得操作集合可相对自由地演化而不影响系统的数据结构。

    4. 符合单一职责原则。访问者模式把相关的行为封装在一起,构成一个访问者,使每一个访问者的功能都比较单一。


    缺点



    1. 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。

    2. 破坏封装。访问者模式中具体元素对访问者公布细节,这破坏了对象的封装性。

    3. 违反了依赖倒置原则。访问者模式依赖了具体类,而没有依赖抽象类。


    结构



    • 抽象访问者(Visitor)角色:定义一个访问具体元素的接口,为每个具体元素类对应一个访问操作 visit() ,该操作中的参数类型标识了被访问的具体元素。

    • 具体访问者(ConcreteVisitor)角色:实现抽象访问者角色中声明的各个访问操作,确定访问者访问一个元素时该做什么。

    • 抽象元素(Element)角色:声明一个包含接受操作 accept() 的接口,被接受的访问者对象作为 accept() 方法的参数。

    • 具体元素(ConcreteElement)角色:实现抽象元素角色提供的 accept() 操作,其方法体通常都是 visitor.visit(this) ,另外具体元素中可能还包含本身业务逻辑的相关操作。

    • 对象结构(Object Structure)角色:是一个包含元素角色的容器,提供让访问者对象遍历容器中的所有元素的方法,通常由 List、Set、Map 等聚合类实现。


    示例


    年底,CEO和CTO开始评定员工一年的工作绩效,员工分为工程师和经理,CTO关注工程师的代码量、经理的新产品数量;CEO关注的是工程师的KPI和经理的KPI以及新产品数量。
    由于CEO和CTO对于不同员工的关注点是不一样的,这就需要对不同员工类型进行不同的处理。访问者模式此时可以派上用场了。


    // 员工基类
    public abstract class Staff {

    public String name;
    public int kpi;// 员工KPI

    public Staff(String name) {
    this.name = name;
    kpi = new Random().nextInt(10);
    }
    // 核心方法,接受Visitor的访问
    public abstract void accept(Visitor visitor);
    }

    Staff 类定义了员工基本信息及一个 accept 方法,accept 方法表示接受访问者的访问,由子类具体实现。Visitor 是个接口,传入不同的实现类,可访问不同的数据。下面看看工程师和经理的代码:


    // 工程师
    public class Engineer extends Staff {

    public Engineer(String name) {
    super(name);
    }

    @Override
    public void accept(Visitor visitor) {
    visitor.visit(this);
    }
    // 工程师一年的代码数量
    public int getCodeLines() {
    return new Random().nextInt(10 * 10000);
    }
    }
    // 经理
    public class Manager extends Staff {

    public Manager(String name) {
    super(name);
    }

    @Override
    public void accept(Visitor visitor) {
    visitor.visit(this);
    }
    // 一年做的产品数量
    public int getProducts() {
    return new Random().nextInt(10);
    }
    }

    工程师是代码数量,经理是产品数量,他们的职责不一样,也就是因为差异性,才使得访问模式能够发挥它的作用。Staff、Engineer、Manager 3个类型就是对象结构,这些类型相对稳定,不会发生变化。
    然后将这些员工添加到一个业务报表类中,公司高层可以通过该报表类的 showReport 方法查看所有员工的业绩,具体代码如下:


    // 员工业务报表类
    public class BusinessReport {

    private List<Staff> mStaffs = new LinkedList<>();

    public BusinessReport() {
    mStaffs.add(new Manager("经理-A"));
    mStaffs.add(new Engineer("工程师-A"));
    mStaffs.add(new Engineer("工程师-B"));
    mStaffs.add(new Engineer("工程师-C"));
    mStaffs.add(new Manager("经理-B"));
    mStaffs.add(new Engineer("工程师-D"));
    }

    /**
    * 为访问者展示报表
    * @param visitor 公司高层,如CEO、CTO
    */
    public void showReport(Visitor visitor) {
    for (Staff staff : mStaffs) {
    staff.accept(visitor);
    }
    }
    }


    下面看看 Visitor 类型的定义, Visitor 声明了两个 visit 方法,分别是对工程师和经理对访问函数,具体代码如下:


    public interface Visitor {

    // 访问工程师类型
    void visit(Engineer engineer);

    // 访问经理类型
    void visit(Manager manager);
    }

    首先定义了一个 Visitor 接口,该接口有两个 visit 函数,参数分别是 Engineer、Manager,也就是说对于 Engineer、Manager 的访问会调用两个不同的方法,以此达成区别对待、差异化处理。具体实现类为 CEOVisitor、CTOVisitor类,具体代码如下:


    // CEO访问者
    public class CEOVisitor implements Visitor {
    @Override
    public void visit(Engineer engineer) {
    System.out.println("工程师: " + engineer.name + ", KPI: " + engineer.kpi);
    }

    @Override
    public void visit(Manager manager) {
    System.out.println("经理: " + manager.name + ", KPI: " + manager.kpi +
    ", 新产品数量: " + manager.getProducts());
    }
    }

    在CEO的访问者中,CEO关注工程师的 KPI,经理的 KPI 和新产品数量,通过两个 visitor 方法分别进行处理。如果不使用 Visitor 模式,只通过一个 visit 方法进行处理,那么就需要在这个 visit 方法中进行判断,然后分别处理,代码大致如下:


    public class ReportUtil {
    public void visit(Staff staff) {
    if (staff instanceof Manager) {
    Manager manager = (Manager) staff;
    System.out.println("经理: " + manager.name + ", KPI: " + manager.kpi +
    ", 新产品数量: " + manager.getProducts());
    } else if (staff instanceof Engineer) {
    Engineer engineer = (Engineer) staff;
    System.out.println("工程师: " + engineer.name + ", KPI: " + engineer.kpi);
    }
    }
    }

    这就导致了 if-else 逻辑的嵌套以及类型的强制转换,难以扩展和维护,当类型较多时,这个 ReportUtil 就会很复杂。而使用 Visitor 模式,通过同一个函数对不同对元素类型进行相应对处理,使结构更加清晰、灵活性更高。
    再添加一个CTO的 Visitor 类:



    public class CTOVisitor implements Visitor {
    @Override
    public void visit(Engineer engineer) {
    System.out.println("工程师: " + engineer.name + ", 代码行数: " + engineer.getCodeLines());
    }

    @Override
    public void visit(Manager manager) {
    System.out.println("经理: " + manager.name + ", 产品数量: " + manager.getProducts());
    }
    }

    重载的 visit 方法会对元素进行不同的操作,而通过注入不同的 Visitor 又可以替换掉访问者的具体实现,使得对元素的操作变得更灵活,可扩展性更高,同时也消除了类型转换、if-else 等“丑陋”的代码。
    下面是客户端代码:


    public class Client {

    public static void main(String[] args) {
    // 构建报表
    BusinessReport report = new BusinessReport();
    System.out.println("=========== CEO看报表 ===========");
    report.showReport(new CEOVisitor());
    System.out.println("=========== CTO看报表 ===========");
    report.showReport(new CTOVisitor());
    }
    }

    具体输出如下:


    =========== CEO看报表 ===========
    经理: 经理-A, KPI: 9, 新产品数量: 0
    工程师: 工程师-A, KPI: 6
    工程师: 工程师-B, KPI: 6
    工程师: 工程师-C, KPI: 8
    经理: 经理-B, KPI: 2, 新产品数量: 6
    工程师: 工程师-D, KPI: 6
    =========== CTO看报表 ===========
    经理: 经理-A, 产品数量: 3
    工程师: 工程师-A, 代码行数: 62558
    工程师: 工程师-B, 代码行数: 92965
    工程师: 工程师-C, 代码行数: 58839
    经理: 经理-B, 产品数量: 6
    工程师: 工程师-D, 代码行数: 53125


    在上述示例中,Staff 扮演了 Element 角色,而 Engineer 和 Manager 都是 ConcreteElement;CEOVisitor 和 CTOVisitor 都是具体的 Visitor 对象;而 BusinessReport 就是 ObjectStructure;Client就是客户端代码。
    访问者模式最大的优点就是增加访问者非常容易,我们从代码中可以看到,如果要增加一个访问者,只要新实现一个 Visitor 接口的类,从而达到数据对象与数据操作相分离的效果。如果不实用访问者模式,而又不想对不同的元素进行不同的操作,那么必定需要使用 if-else 和类型转换,这使得代码难以升级维护。


    应用场景


    当系统中存在类型数量稳定(固定)的一类数据结构时,可以使用访问者模式方便地实现对该类型所有数据结构的不同操作,而又不会对数据产生任何副作用(脏数据)。


    简而言之,就是当对集合中的不同类型数据(类型数量稳定)进行多种操作时,使用访问者模式。


    通常在以下情况可以考虑使用访问者模式。



    1. 对象结构相对稳定,但其操作算法经常变化的程序。

    2. 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构。

    3. 对象结构包含很多类型的对象,希望对这些对象实施一些依赖于其具体类型的操作。  

    收起阅读 »

    5分钟快速使用MQTT客户端连接环信MQTT消息云

    本文介绍如何使用MQTT客户端快速连接环信MQTT消息云一.操作流程 1、开通MQTT业务 开通环信MQTT消息云服务见快速开通MQTT服务;2、下载MQTT客户端 常见的MQTT客户端整理如下,下载客户端后可快速连接环信MQTT...
    继续阅读 »

    本文介绍如何使用MQTT客户端快速连接环信MQTT消息云

    一.操作流程 

    1、开通MQTT业务 
    开通环信MQTT消息云服务见快速开通MQTT服务

    2、下载MQTT客户端 

    常见的MQTT客户端整理如下,下载客户端后可快速连接环信MQTT消息云:

    MQTT客户端 操作系统 下载地址 
     MQTT Explorer Windows,macOS,Linux点击下载
     MQTT.fx Windows,macOS,Linux点击下载
     MQTT Box Windows,macOS,Linux点击下载
     mqtt-spy Windows,macOS,Linux点击下载
     Mosquitto CLI Windows,macOS,Linux点击下载


    二.接入指引
     

     1、连接五要素 
    MQTT客户端在连接环节需要5个基本参数,包括连接地址(Host)、端口(Port)、clientID(MQTT client ID)、用户ID(Username)、token(Password)。 
    获取方式如下:
    step1.进入console控制台,选择【MQTT】->【服务概览】;
    step2.获取clientID,clientID由两部分组成,组织形式为“deviceID@AppID”,deviceID由用户自定义,AppID见【服务配置】下AppID; 
    step3.获取连接地址(Host); 
    step4.获取端口(Port); 
    step5.选择左侧菜单栏【应用概览】->【用户认证】; 
    step6.获取用户ID(Username); 
    step7.获取token(Password); 




    2、连接环信MQTT消息云
    本文以MQTT Explorer for MAC版本为例(可通过APP Store下载)。打开MQTT客户端软件,选择“+”新建图标。



    step1.用户自定义连接名称; 
    step2.是否选择开启tls加密,取值:“开启”、“关闭”;
    step3.选择连接协议,取值:“ws:(websocket)”、“mqtt:”,若step2选择开启tls,协议为“wss:”、“mqtts”; 
    step4.填写环信MQTT消息云连接地址(Host); 
    step5.填写端口(Port); 
    step6.填写用户ID(username); 
    step7.填写token(Password); 
    step8.选择【ADVANCE】,填写clientID,clientID由两部分组成,“deviceID@AppID”; 
    step9.填写订阅主题名称,此例为“t/t1” ;
    step10.填写后点击【ADD】按钮添加至订阅列表中; 
    step11. 填写clientID名称; 
    step12. 选择【BACK】按钮,返回至主页面;
    step13. 选择主页面中【CONNECT】即可连接成功;



    3、订阅/发布消息 
    在创建一个MQTT 客户端,执行【连接环信MQTT消息云】流程;

    【发布消息】
    step1.填写发布的主题,本例中为“/t/t1”; 
    step2.选择消息体格式,取值:“raw”、“xml”、“json”;
    step3.填写消息体内容,本例中为“hello world”; 
    step4.选择QoS等级,取值:“0:至多发送一次,不保留”、“1:至少一次,保留”、“2:仅发一次,保留”; 
    step5.选择是否为保留消息,取值:“0:不保留”、“1:保留,订阅客户端重新接入环信MQTT消息云时,可以接收保留消息”; 
    step6.发送消息; 


    【订阅消息】
    订阅/t/t1的MQTT客户端即可接收消息

    收起阅读 »

    Swift是否可以集成环信IM SDK?

    可以。Swift集成SDK时,在自定义cell和EaseIMKit混用时,会导致程序崩溃问题.原因是,无法返回nil,应该怎么处理?解决方法:让用户集成easeIMKit源码,将创建自定义cell回调的返回值添加一个可为空的关键字nullable

    可以。

    Swift集成SDK时,在自定义cell和EaseIMKit混用时,会导致程序崩溃问题.原因是,无法返回nil,应该怎么处理?
    解决方法:让用户集成easeIMKit源码,将创建自定义cell回调的返回值添加一个可为空的关键字nullable

    OC对象的本质(上) —— OC对象的底层实现原理

    一个NSObject对象占用多少内存?Objective-C的本质平时我们编写的OC代码,底层实现都是C/C++代码Objective-C --> C/C++ --> 汇编语言 --> 机器码所以Objective-C的面向对象都是基于C/C...
    继续阅读 »

    一个NSObject对象占用多少内存?

    Objective-C的本质
    平时我们编写的OC代码,底层实现都是C/C++代码

    Objective-C --> C/C++ --> 汇编语言 --> 机器码

    所以Objective-C的面向对象都是基于C/C++的数据结构实现的,所以我们可以将Objective-C代码转换成C/C++代码,来研究OC对象的本质。

    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    NSObject *obj = [[NSObject alloc] init];
    }
    return 0;
    }

    我们在main函数里面定义一个简单对象,然后通过 clang -rewrite-objc main.m -o main.cpp命令,将main.m文件进行重写,即可转换出对应的C/C++代码。但是可以看到一个问题,就是转换出来的文件过长,将近10w行。


    因为不同平台支持的代码不同(Windows/Mac/iOS),那么同样一句OC代码,经过编译,转成C/C++代码,以及最终的汇编码,是不一样的,汇编指令严重依赖平台环境。
    我们当前关注iOS开发,所以,我们只需要生成iOS支持的C/C++代码。因此,可以使用如下命令
    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc <OC源文件> -o <输出的cpp文件>
    -sdk:指定sdk
    -arch:指定机器cpu架构(模拟器-i386、32bit、64bit-arm64 )
    如果需要链接其他框架,使用-framework参数,比如-framework UIKit
    一般我们手机都已经普及arm64,所以这里的架构参数用arm64,生成的cpp代码如下



    接下来,我们查看一下main_arm64.cpp源文件,如果熟悉这个文件,你将会发现这么一个结构体

    struct NSObject_IMPL {
    Class isa;
    };

    我们再来对比看一下NSObject头文件的定义

    @interface NSObject <NSObject> {
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa OBJC_ISA_AVAILABILITY;
    #pragma clang diagnostic pop
    }
    @end

    简化一下,就是

    @interface NSObject  {
    Class isa ;
    }
    @end

    是不是猜到点什么了?没错,struct NSObject_IMPL其实就是NSObject的底层结构,或者说底层实现。换个角度理解,可以说C/C++的结构体类型支撑了OC的面相对象。

    点进Class的定义,我们可以看到 是typedef struct objc_class *Class;

    Class isa; 等价于 struct objc_class *isa;

    所以NSObject对象内部就是放了一个名叫isa的指针,指向了一个结构体 struct objc_class。

    总结一:一个OC对象在内存中是如何布局的?


    猜想:NSObject对象的底层就是一个包含了一个指针的结构体,那么它的大小是不是就是8字节(64位下指针类型占8个字节)?
    为了验证猜想,我们需要借助runtime提供的一些工具,导入runtime头文件,class_getInstanceSize ()方法可以计算一个类的实例对象所实际需要的的空间大小

    #import <Foundation/Foundation.h>
    #import <objc/runtime.h>
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    NSObject *obj = [[NSObject alloc] init];
    size_t size = class_getInstanceSize([NSObject class]);
    NSLog(@"NSObject对象的大小:%zd",size);
    }
    return 0;
    }

    结果是


    完美验证,it's over,let's go home!


    等等,就这么简单?确定吗?答案是否定的~~~
    介绍另一个库#import <malloc/malloc.h>,其下有个方法 malloc_size(),该函数的参数是一个指针,可以计算所传入指针 所指向内存空间的大小。我们来用一下

    #import <Foundation/Foundation.h>
    #import <objc/runtime.h>
    #import <malloc/malloc.h>

    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    NSObject *obj = [[NSObject alloc] init];
    size_t size = class_getInstanceSize([NSObject class]);
    NSLog(@"NSObject实例对象的大小:%zd",size);
    size_t size2 = malloc_size((__bridge const void *)(obj));
    NSLog(@"对象obj所指向的的内存空间大小:%zd",size2);
    }
    return 0;
    }

    结果是16,如何解释呢?


    想要真正弄清楚其中的缘由,就需要去苹果官方的开源代码里面去一探究竟了。苹果的开源代请看这里。
    先看一下class_getInstanceSize的实现。我们需要进到objc4/文件里面下载一份最新的源码,我当前最新的版本是objc4-750.1.tar.gz。下载解压之后,打开工程,就可以查看runtime的实现源码。
    搜索class_getInstanceSize找到实现代码

    size_t class_getInstanceSize(Class cls)
    {
    if (!cls) return 0;
    return cls->alignedInstanceSize();
    }

    再点进alignedInstanceSize方法的实现

    // Class's ivar size rounded up to a pointer-size boundary.
    uint32_t alignedInstanceSize() {
    return word_align(unalignedInstanceSize());
    }

    可以看到该方法的注释说明Class's ivar size rounded up to a pointer-size boundary.,意思就是获得类的成员变量的大小,其实也就是计算类所对应的底层结构体的大小,注意后面的这个rounded up to a pointer-size boundary指的是系统在为类的结构体分配内存时所进行的内存对齐,要以一个指针的长度作为对齐系数,64位系统指针长度(字长)是8个字节,那么返回的结果肯定是8的最小整数倍。为什么需要用指针长度作为对齐系数呢?因为类所对应的结构体,在头部的肯定是一个isa指针,所以指针肯定是该结构体中最大的基本数据类型,所以根据结构体的内存对齐规则,才做此设定。如果对这里有疑惑的话,请先复习一下有关内存对齐的知识,便一目了然了。
    所以class_getInstanceSize方法,可以帮我们获取一个类的的实例对象所对应的结构体的实际大小。

    我们再从alloc方法探究一下,alloc方法里面实际上是AllocWithZone方法,我们在objc源码工程里面搜索一下,可以在Object.mm文件里面找到一个_objc_rootAllocWithZone方法。

    id _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
    {
    id obj;

    #if __OBJC2__
    // allocWithZone under __OBJC2__ ignores the zone parameter
    (void)zone;
    obj = class_createInstance(cls, 0);
    #else
    if (!zone) {
    obj = class_createInstance(cls, 0);
    }
    else {
    obj = class_createInstanceFromZone(cls, 0, zone);
    }
    #endif

    if (slowpath(!obj)) obj = callBadAllocHandler(cls);
    return obj;
    }

    再点进里面的关键方法class_createInstance的实现看一下

    id  class_createInstance(Class cls, size_t extraBytes)
    {
    return _class_createInstanceFromZone(cls, extraBytes, nil);
    }

    继续点进_class_createInstanceFromZone方法

    static __attribute__((always_inline)) 
    id
    _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
    bool cxxConstruct = true,
    size_t *outAllocatedSize = nil)
    {
    if (!cls) return nil;

    assert(cls->isRealized());

    // Read class's info bits all at once for performance
    bool hasCxxCtor = cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();

    size_t size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (!zone && fast) {
    obj = (id)calloc(1, size);
    if (!obj) return nil;
    obj->initInstanceIsa(cls, hasCxxDtor);
    }
    else {
    if (zone) {
    obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
    } else {
    obj = (id)calloc(1, size);
    }
    if (!obj) return nil;

    // Use raw pointer isa on the assumption that they might be
    // doing something weird with the zone or RR.
    obj->initIsa(cls);
    }

    if (cxxConstruct && hasCxxCtor) {
    obj = _objc_constructOrFree(obj, cls);
    }

    return obj;
    }

    这个方法有点长,有时分析一个方法,不要过分拘泥细节,先针对我们寻找的问题,找到关键点,像这个比较长的方法,我们知道,它的主要功能就是创建一个实例,为其开辟内存空间,我们可以发现中间的这句代码obj = (id)calloc(1, size);,是在分配内存,这里的size是需要分配的内存的大小,那这句应该就是为对象开辟内存的核心代码,再看它里面的参数size,我们能在上两行代码中找到size_t size = cls->instanceSize(extraBytes);,于是我们继续点进instanceSize看看

    size_t instanceSize(size_t extraBytes) {
    size_t size = alignedInstanceSize() + extraBytes;
    // CF requires all objects be at least 16 bytes.
    if (size < 16) size = 16;
    return size;
    }

    翻译一下这句注//CF requires all objects be at least 16 bytes.我们就明白了,CF作出了硬性的规定:当创建一个实例对象的时候,为其分配的空间不能小于16个字节,为什么这么规定呢,我个人目前的理解是这可能就相当于一种开发规范,或者对于CF框架内部的一些实现提供的规范。
    这个size_t instanceSize(size_t extraBytes)返回的字节数,其实就是为 为一个类创建实例对象所需要分配的内存空间。这里我们的NSObject类创建一个实例对象,就分配了16个字节。
    我们在点进上面代码中的alignedInstanceSize方法

    // Class's ivar size rounded up to a pointer-size boundary.
    uint32_t alignedInstanceSize() {
    return word_align(unalignedInstanceSize());
    }

    这不就是我们上面分析class_getInstanceSize方法里面看到的那个alignedInstanceSize嘛。


    总结二:class_getInstanceSize&malloc_size的区别

    class_getInstanceSize:获取一个objc类的实例的实际大小,这个大小可以理解为创建这个实例对象至少需要的空间(系统实际为这个对象分配的空间可能会比这个大,这是出于系统内存对齐的原因)。
    malloc_size:得到一个指针所指向的内存空间的大小。我们的OC对象就是一个指针,利用这个函数,我们可以得到该对象所占用的内存大小,也就是系统为这个对象(指针)所指向对象所实际分配的内存大小。
    sizeof():获取一个类型或者变量所占用的存储空间,这是一个运算符。
    [NSObject alloc]之后,系统为其分配了16个字节的内存,最终obj对象(也就是struct NSObject_IMPL结构体),实际使用了其中的8个字节内存,(也就是其内部的那个isa指针所用的8个字节,这里我们是在64位系统为前提下来说的)
    关于运算符和函数的一些对比理解

    函数在编译完之后,是可以在程序运行阶段被调用的,有调用行为的发生
    运算符则是在编译按一刻,直接被替换成运算后的结果常量,跟宏定义有些类似,不存在调用的行为,所以效率非常高

    更为复杂的自定义类

    我们开发中会自定义各种各样的类,基本上都是NSObject的子类。更为复杂的子类对象的内存布局又是如何的呢?我们新建一个NSObject的子类Student,并为其增加一些成员变量

    @interface Student : NSObject
    {
    @public
    int _age;
    int _no;
    }

    @end

    @implementation Student

    @end

    使用我们之前介绍过的方法,查看一下这个类的底层实现代码

    struct NSObject_IMPL {
    Class isa;
    };

    struct Student_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _age;
    int _no;

    };

    我们发现其实Student的底层结构里,包含了它的成员变量,还有一个NSObject_IMPL结构体变量,也就是它的父类的结构体。根据我们上面的总结,NSObject_IMPL结构体需要的空间是8字节,但是系统给NSObject对象实际分配的内存是16字节,那么这里Student的底层结构体里面的成员变量NSObject_IMPL应该会得到多少的内存分配呢?我们验证一下。

    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    NSObject *obj = [[NSObject alloc] init];
    //获取`NSObject`类的实例对象的成员变量所占用的大小
    size_t size = class_getInstanceSize([NSObject class]);
    NSLog(@"NSObject实例对象的大小:%zd",size);
    //获取obj所指向的内存空间的大小
    size_t size2 = malloc_size((__bridge const void *)(obj));
    NSLog(@"对象obj所指向的的内存空间大小:%zd",size2);

    Student * std = [[Student alloc]init];
    size_t size3 = class_getInstanceSize([Student class]);
    NSLog(@"Student实例对象的大小:%zd",size3);
    size_t size4 = malloc_size((__bridge const void *)(std));
    NSLog(@"对象std所指向的的内存空间大小:%zd",size4);
    }
    return 0;
    }


    貌似是对的了,但是为什么用malloc_size得到std所被分配的内存是32?再来一发试试

    @interface Student : NSObject
    {

    @public
    //父类的isa还会占用8个字节
    int _age;//4字节
    int _no;//4字节
    int _grade;//4字节
    int *p1;//8字节
    int *p2;//8字节
    }

    Student结构体所有成员变量所需要的总空间为 36字节,根据内存对齐原则,最后结构体所需要的空间应该是8的倍数,那应该就是40,我们看一下结果


    从结果看没错,但是同时也发现了一个规律,随着std对象成员变量的增加,系统为Student对象std分配的内存空间总是以16的倍数增加(16~32~48......),我们之前分析源码好像没看到有做这个设定


    其实上面这个方法只是可以用来计算一个结构体对象所实际需要的内存大小。 [update]其实instanceSize()-->alignedInstanceSize()只是可以用来计算一个结构体对象理论上(按照内存对其规则)所需要分配的内存大小。

    真正给实例对象完成分配内存操作的是下面这个方法calloc()


    这个方法位于苹果源码的libmalloc文件夹中。但是里面的代码再往下深究,介于我目前的知识储备以及专业出身(数学专业),还是困难比较大。好在从一些大神那里得到了指点。
    刚才文章开始,我们讨论到了结构体的内存对齐,这是针对数据结构而言的。从系统层面来说,就以苹果系统而言,出于对内存管理和访问效率最优化的需要,会实现在内存中规划出很多块,这些块有大有小,但都是16的倍数,比如有的是32,有的是48,在libmalloc源码的nano_zone.h里面有这么一段代码

    #define NANO_MAX_SIZE    256 /* Buckets sized {16, 32, 48, 64, 80, 96, 112, ...} */

    NANO是源码库里面的其中一种内存分配方法,类似的还有frozen、legacy、magazine、purgeable。


    这些是苹果基于各种场景优化需求而设定的对应的内存管理相关的库,暂时不用对其过分解读。
    上面的NANO_MAX_SIZE解释中有个词Buckets sized,就是苹果事先规划好的内存块的大小要求,针对nano,内存块都被设定成16的倍数,并且最大值是256。举个例子,如果一个对象结构体需要46个字节,那么系统会找一块48字节的内存块分配给它用,如果另一个结构体需要58个字节,那么系统会找一块64字节的内存块分配给它用。
    到这里,应该就可以基本上解释清楚,为什么刚才student结构需要40个字节的时候,被分配到的内存大小确实48个字节。至此,针对一个NSObject对象占用内存的问题,以及延伸出来的内存布局,以及其子类的占内存问题,应该就都可以得到解答了。

    OC对象的本质(上):OC对象的底层实现
    OC对象的本质(中):OC对象的分类
    OC对象的本质(下):详解isa&superclass指针

    面试题解答

    一个NSObject对象占用多少内存?
    1)系统分配了16字节给NSObject对象(通过malloc_size函数可以获得)
    2)NSObject对象内部只使用了8个字节的空间,用来存放isa指针变量(64位系统下,可以通过class_getInstanceSize函数获得)

    链接:https://www.jianshu.com/p/1bf78e1b3594

    收起阅读 »

    iOS内存(Heap堆内存 && Anonymous VM 虚拟内存) 分析和理解

    在使用Instruments 做内存分析的时候, 我们会看到如下的画面,箭头指向的地方有堆内存heap Allocations,和虚拟内存 Anonymous VM , 到底在手机上什么是堆内存,什么是虚拟内存 Anonymous VM 呢? 在观察内存分配的...
    继续阅读 »

    在使用Instruments 做内存分析的时候, 我们会看到如下的画面,箭头指向的地方有堆内存heap Allocations,和虚拟内存 Anonymous VM , 到底在手机上什么是堆内存,什么是虚拟内存 Anonymous VM 呢? 在观察内存分配的时候 我们是否需要
    去了解它

    前言所需要的图片(如下图)


    1) 什么是堆
    堆是一种完全结构的二叉树 堆与二叉树的理解

    堆: 是大家共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程 初始化的时候分配,运行过程中也可以向系统要额外的堆,但是记得用完了要还给操作系统,要不然就是内存泄漏。堆里面一般 放的是静态数据,比如static的数据和字符串常量等,资源加载后一般也放在堆里面。一个进程的所有线程共有这些堆 ,所以对堆的操作要考虑同步和互斥的问题。程序里面编译后的数据段都是堆的一部分。

    — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表

    {
    int b; //栈
    char s[] = "abc"; //栈
    char *p2; //栈
    char *p3 = "123456"; //123456{row.content}在常量区,p3在栈上。
    static int c =0; //全局(静态)初始化区
    p1 = (char *)malloc(10);
    p2 = (char *)malloc(20);//分配得来的10和20字节的区域就在堆区。
    strcpy(p1, "123456"); //123456{row.content}放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。}

    堆:首 先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结 点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才 能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

    堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出分配方式:堆都是动态分配的,没有静态分配的堆。

    1.1) 堆上消耗的内存


    1、View 函数的调用
    2、注册通知
    3、抛出通知
    4、view 的布局
    5、函数代码的执行
    6、sqlite 数据库的创建
    7、向字典中增加对象


    8、等等

    都需要消耗内存, 上面的代码都是程序员创建的, 程序员去控制堆的内存

    1.2) 堆上的内存是否释放

    1.2.1) 已经释放的例子:

    点击步骤1)箭头


    查看步骤2)箭头


    步骤2) 箭头中有 free 函数, 可以看出, 这个对象 已经被释放

    1.2.2) 堆上内存不释放的例子:


    上图中箭头执行的地方 没有free 函数 说明 这个对象已经释放

    2) Anonymous VM

    2.1) 苹果官方文档对虚拟内存的解释

    更小的内存消耗不仅可以减少内存, 还可以减少cpu 的时间
    我们可能会看到这样的情况, All Heap Allocations 是程序真实的内存分配情况,All Anonymous VM则是系统为程序分配的虚拟内存,为的就是当程序有需要的时候,能够及时为程序提供足够的内存空间,而不会现用现创建

    Anonymous VM内存是虚拟内存
    、All Anonymous VM。我们无法控制Anonymous VM部分 ,(更新,其实还是可以优化 比如图片绘制相关 详情参见iOS内存探究,需要对虚拟内存熟悉 才能优化)

    2.2) 问题: 我们需要关注Anonymous VM 内存吗 ?
    问答连接
    Should you focus on the Live Bytes column for heap allocations or anonymous VM? Focus on the heap allocations because your app has more control over heap allocations. Most of the memory allocations your app makes are heap allocations.

    The VM in anonymous VM stands for virtual memory. When your app launches, the operating system reserves a block of virtual memory for your application. This block is usually much larger than the amount of memory your app needs. When your app allocates memory, the operating system allocates the memory from the block it reserved.

    Remember the second sentence in the previous paragraph. The operating system determines the size of the virtual memory block, not your app. That’s why you should focus on the heap allocations instead of anonymous VM. Your app has no control over the size of the anonymous VM.

    2.3) 不需要关注 Anonymous VM
    我们应该关注堆内存, 因为我们对堆内存有更大的掌控, 大部分我们在app的内存分配是堆内存

    VM 在匿名空间中代表的是虚拟内存, 当你的app启动的时候, 操作系统为你的应用程序分配内存, 这个分配的虚拟内存一般比你的app需要的内存大很多,

    操作系统决定虚拟内存的分配, 而不是你的app, 这就是你为什么要集中精力处理堆内存, 你的app 对虚拟内存没有掌控力

    2.4) 虚拟内存过大 (未解之谜) 如果知道结果请评论留言, 多谢


    CGBitmapContextCreateImage 函数会导致虚拟内存过大 ,并且还不释放, 用法未发现问题

    CGImageRef alphaMaskImage = CGBitmapContextCreateImage(alphaOnlyContext);
    UIImage *result = [UIImage imageWithCGImage:alphaMaskImage scale:image.scale orientation:image.imageOrientation];
    CGImageRelease(alphaMaskImage);
    CGContextRelease(alphaOnlyContext);
    return result;

    参考文献

    iOS中的堆(heap)和栈(stack)的理解

    苹果虚拟内存的官方文档

    转自:https://www.jianshu.com/p/dffd5c24dc9a

    收起阅读 »

    【环信IM集成指南】iOS端常见问题整理

    建议用浏览器搜索定位问题~本文持续更新,欢迎大家留言点菜~1、集成IM如何自定义添加表情组https://www.imgeek.org/article/8253575062、旧版音视频与EaseCallKit兼容升级方案https://www.imgeek.o...
    继续阅读 »
    建议用浏览器搜索定位问题~
    本文持续更新,欢迎大家留言点菜~


    1、集成IM如何自定义添加表情组

    https://www.imgeek.org/article/825357506


    2、旧版音视频与EaseCallKit兼容升级方案

    https://www.imgeek.org/article/825357507

     
    3、如何集成环信EaseIMKit和EaseCallKit源码

    https://www.imgeek.org/article/825357493

     
    4、解决集成EaseIMKit源码后没有图片的问题

    https://www.imgeek.org/article/825357495

     
    5、EaseIMKit如何设置昵称、头像

    https://www.imgeek.org/article/825354241

     
    6、Swift是否可以集成环信IM SDK?

    https://www.imgeek.org/article/825357511

     
    7、环信IM会话列表和聊天界面修改头像和昵称

    https://www.imgeek.org/article/825357608

     
    8、手把手教集成EaseIMKit源码 

    https://www.imgeek.org/article/825357673


    9、环信聊天室如何每次进来可以看到之前的已读消息

    https://imgeek.org/article/825357723


    10、这几个iOS拓展字段,是只对iOS生效吗?对安卓没有影响吧?
    em_push_content 自定义推送显示

    em_push_category 向 APNs Payload 中添加 category 字段
    em_push_sound 自定义推送提示音
    em_push_mutable_content 开启 APNs 通知扩展
    em_ignore_notification 发送静默消息
    em_force_notification 设置强制推送型 APNs

    答:下面这三个对安卓不生效,其他的是两端都会起作用。
    em_push_category、em_push_sound、em_push_mutable_content

    11、无网时发送消息,然后迅速切到有网状态。这时显示发送成功,然后回退到上一个页面再进入到IM页,刚刚那条消息被重复发送了


    可以开通服务端消息去重功能。


    12、如果设置了离线不踢出聊天室,那聊天室的消息会有离线推送吗?

    聊天室没有离线处理,所以没有离线推送。


    13、cmd不进行漫游功能配置成功之前的历史消息,在配置好之后还是能拉下来的。


    14、iOS和安卓端发视频消息,对视频格式有要求吗?
    答:没有


    15.图片发送设置了缩略图,收到的消息里面没有缩略图,只有源文件数据



    接收方会直接将缩略图下载到本地,SDK会自动把缩略图缓存到本地,您直接通过body.thumbnailLocalPath就可以获取到了, 我们的UI SDK已经对这些做了封装,不需要您再单独进行处理,如果您这边就是想拿到这张缩略图来使用的话,就需要在messagesDidReceive方法里面自己再判断一下,如果是图片消息的话,就去打印缩略图的路径,然后通过这个路径可以获取到缩略图的原图

    case EMMessageBodyTypeImage:
    {
    // 得到一个图片消息body
    EMImageMessageBody *body = ((EMImageMessageBody *)msgBody);
    NSLog(@"大图remote路径 -- %@" ,body.remotePath);
    NSLog(@"大图local路径 -- %@" ,body.localPath); // // 需要使用sdk提供的下载方法后才会存在
    NSLog(@"大图的secret -- %@" ,body.secretKey);
    NSLog(@"大图的W -- %f ,大图的H -- %f",body.size.width,body.size.height);
    NSLog(@"大图的下载状态 -- %lu",body.downloadStatus);


    // 缩略图sdk会自动下载
    NSLog(@"小图remote路径 -- %@" ,body.thumbnailRemotePath);
    NSLog(@"小图local路径 -- %@" ,body.thumbnailLocalPath);
    NSLog(@"小图的secret -- %@" ,body.thumbnailSecretKey);
    NSLog(@"小图的W -- %f ,大图的H -- %f",body.thumbnailSize.width,body.thumbnailSize.height);
    NSLog(@"小图的下载状态 -- %lu”,body.thumbnailDownloadStatus);


    16.后端该如何操作用户上麦

    后端无法直接控制让谁上麦,所以只能通过发送CMD消息的方式来和移动端进行交互,移动端根据逻辑指令去操作

    17.使用[[EMClient sharedClient].chatManager ackConversationRead:_conversation.conversationId completion:nil];将消息置为已读,但是还是有未读数

    [[EMClient sharedClient].chatManager ackConversationRead:_conversation.conversationId completion:nil]; —- 这个方法是发送会话已读消息,将通知服务器将此会话未读数置为0,而将消息置为已读是本地操作,可以使用方法:
    1).[[EaseIMKitManager shared] markAllMessagesAsReadWithConversation:_conversation];
    2). [conversation markMessageAsReadWithId:message.messageId error:nil];
    3).[conversation markMessageAsReadWithId:message.messageId error:nil];
    注意:方法1是EaseIMKitManager调用的,方法2、3是EMConversation调用的

    18.聊天页面头像设置圆角失败
    如果要设置聊天页面头像的圆角值,需要先设置avatarType为圆形才会生效,如果想要设置为圆形,则直接给图片宽度的一半即可


    19.调用getGroupSpecificationFromServerWithId获取群组详情失败,失败的原因 --- you have no permission to do this, group member permission is required

    出现此问题的原因是当前用户不在群组内,获取群组详情必须是群组成员才有权限,如果因为场景特殊的话,可以使用rest接口获取。

    20、如何将附件保存在自己的服务器上

    1.项目中搜索:isAutoTransferMessageAttachments,将属性值改为no
    2.用户上传文件完成后,不建议用户直接使用remotePath,而是使用ext扩展来存放文件链接.


    21、请问后台和sdk对群组名称和群组描述,有字数或其他限制吗?分别是多少?
    后台:名称 16字符 超出部分截去,描述64字符 超出部分截去
    Sdk:无限制


    22、全局广播相关:
    (1)支持发送自定义类型消息和扩展消息吗?

    支持。
    (2)会有离线推送延迟的问题吗?
    会,慢速堆积,就会延迟。延迟15分钟很正常的。
    (3)全局广播的延迟是根据用户量来的,按每秒下发1000个来推,如果有用户1万个,预计需要10秒。

    23、同一个环信id在多设备登录,可以同时加入同一个聊天室。但设备数量有限制,根据多端多设备功能配置的数量来。


    24、p8证书在开发和生产环境下都可以工作(不需要在证书之间切换),最重要的是,它不会过期!


    25、console后台添加推送证书有数量限制吗?

    无限制。但不要短时间内快速上传大量证书。

    26、获取token的接口,是根据ip做限制的。例如一个ip,每秒最多10次。

    27、iOS端对于离线推送扩展字段:em_push_title、em_push_content的显示逻辑。
    如果title和content都有,就显示title的,没有title就取content的值,两个是有优先级的。
    如果想要标题和内容都有的样式,可以只用em_push_content,然后将内容进行换行


    28、群消息可以单独指定给某人吗?
    我们没有这个功能,您可以自己实现。
    消息带上扩展,可以是指定人的环信id,群成员们收到消息(messagesDidReceive)后判断扩展内容是不是自己的环信id,是的话就展示,不是就不展示。


    29、如果同时设置了发送前和发送后回调,会先执行发送前,再执行发送后。

    30、回调会保证顺序发送吗?
    回调不保证发送顺序 消息里面都是带时间的。


    31、自定义的聊天cell,在哪里设置cell 的高度?
    自定义cell的高度是自动计算的,自适应的,正常不用单独设置。
    如果有问题,看下自定义的cell的布局是不是不对。


    32、从服务器端获取会话列表功能相关规则:

    (1)、时效是7天(社区、企业等版本都是统一的)。
    如果购买了消息漫游,会话列表保存时长延长至购买的漫游时长。
    也可以单独延长保存时长,收费相关需要和商务沟通。
    (2)、只获取到会话的最新一条消息,要获取这个会话的其他历史消息可以再调用漫游
    (3)、调用后会自动同步到本地数据库(app端)
    (4)、默认可以获取10个会话,最大可以上调到100个。需要联系商务调整
    (5)、cmd消息不计入会话列表
    (6)、开通后需要发送新的消息测试,开通前的数据获取不到


    33、群组全员禁言、将某成员解禁,此成员还是无法发消息
    这个现象是正常的。


    34、图片消息的大图、缩略图的服务器端路径为什么是一样的?
    这是正常的,对于服务器端来说,下载缩略图就是多个参数,sdk下载时会有区分。



    35、发消息超时重试机制
    (1)、断网的情况下发消息,30秒后直接返回error消息
    (2)、弱网的情况下,发送附件类型消息需要先进行上传,调用 rest接口,60秒 + 60秒重试,2min后返回error消息
    (3)、弱网的情况下,发送非附件类型消息直接走mysnc,1min后返回error消息


    36. 大小写敏感的问题
    Q:获取不到会话~
    A: 大小写敏感
    message里面 conversation:WePlay_eTl36Lbp***LQRbX6CzL-
    conversation数据库里面conversation: weplay_etl36lbp***lqrbx6czl-

    37. Q: 请问一下,SDK发送文件、图片、视频的时候,默认是存储在你们的服务器上的吗?存储地址是否可以自定义?
    A: 是可以的
    1. 关闭环信的自动下载或者上传附件isAutoTransferMessageAttachments
    2. 发送正常消息的时候,在ext里面加上传到自己服务器的资源地址、和文件类型

    38. sdk 报300 

    是客户端连不上msync服务,这个分两种情况 1.客户端自身网络问题,比如设置了代理服务、网络异常 2.环信服务异常 客户端连接的msync服务异常 提示300


    39. 发送方网络不好时发了一条消息,接收方收到两条。
    原因:就是网络不好,消息发出去后,sdk没有收到服务器端的ack,sdk认为消息没有发送成功,然后又发了一条。
    Q:如果客户的场景就是短时间内重复发相同内容的消息,那配置了这个去重,是不是就会把重复的消息也过滤掉?
    A:不会。正常情况下,发相同的消息,每条消息的metaid不同,异常情况下metaid是相同的。meta id就是sdk本地临时生成的消息id,就是网络不好,消息发出去后,sdk没有收到服务器端的ack,sdk认为消息没有发送成功,然后又发了一条
    就是这种情况,涉及到sdk重发消息,所以两条消息的metaid是相同的
    此情况可以配置“服务端消息去重 unique_client_msgid”,或者联系运维配置。

    40. EaseCallKit 问题
    Q: 声网的音视频发送消息类型全部是EMMessageTypeCmd 吗?

    A: 音视频通话过程中的第一条呼叫邀请是文本消息,其他都是cmd消息

    41. EaseCallKit 问题
    EMMessageTypePictMixText是用来标记图片类型的,在现在的项目里面是没有用到的

    42. 场景:
    Q: 现在有个这样的场景,客户端申请创建群服务审核后由服务端创建了一个群,还有就是客户端也可以申请加入群,加入成功和创建成功后都会收到这个回调,我有办法区分出来是服务端创建的群还是我自己主动加入的群么?

    A: 服务端在创建群组的时候,有一个custom字段,您可以做业务相关的标记~ custom字段对应客户端的字段是:EMGroup -> ext字段

    43. iOS应用的强杀,在聊天室里的人需要立马能感受到他的离线。或者离开。推荐怎么做好
    A: 正常情况是离线2min,服务器会将此成员踢出聊天室,2min的时间在我们这儿是可以设置的

    44、 加个 em_push_name 有没有用?
    A: iOS 目前不支持em_push_name,解决方案是通过\n,来模拟类似标题的效果



    45.、用户如果杀进程,你们日志里面会添加记录吗
    A: 杀死就是直接log突然没记录了



    46、我们的SDK,自动登录,是有token校验的机制的?
    我在A设备登录完,又在B设备登录了
    这个时候,我又从A设备自登录了,A这边能感知到token变化?(或者说已经在别的设备登录过了)
    A: 这个需求研发已经在排期做,完成后的预期时A离线再上线时也可以感知到有其他设备登陆过

    47、群组的代理在EaseIMKitManager里面可以走,在help类里面是不走的,需要进一步核实~

    48、为啥不用UUID用作环信ID?
    A:环信最初设计是来源注册规则,其中username是现在客户注册的环信ID,环信系统收到这个username后会自动生成一个内部的UUID,所以不允许用户使用UUID作为环信ID,避免冲突。username是64位的,UUID是36位的,客户可以在UUID的前边加个前缀作为username。


    49、 the resource could not be loaded because the App Transport Security policy requires the use of a secure connection.
    A: 在app的info.plist 文件中,设置Allow Arbitrary Loads = yes
    或者是在EMOptions 调用usingHttpsOnly = YES 仅支持https

    flutter问题
    50、语音播放

    使用RecordAmr,播放remotePath,安卓可以、iOS不可以
    目前给的解决方案:把remotePath修改成localPath


    51、添加回调规则添加失败。
    A:检查下回调规则名称是不是用的汉字,回调规则只能是数字、字母,不能用汉字。


    52、对方离线了之后,发送的消息,上线后如何获取?
    A:对方离线,消息会进入离线队列,如果没有集成第三方厂商离线推送,用户上线后,服务器下发给客户端。


    53、调用SDK 方法报错: Cannot read property 'lookup' of undefined?
    A:因为未登陆成功就调用了SDK 的api,需要在onOpened 链接成功回调执行后再去调用SDK 的api。


    54、聊天室如何获取历史消息?
    A:两种方式:1、环信服务器端主动推,需要联系商务开通服务,默认10条,数量可以调整。2、通过消息漫游接口自己去拉取历史消息,各端都有提供拉取漫游消息接口。


    55、拉取消息漫游,conversationId是怎么获取的?
    A:单聊的话,conversationId 就是对方用户的环信id。
    群聊或聊天室的话,conversationId 就是groupid 或者chatroomid。


    56、如何实现只有好友才可以发消息?
    A:可以使用环信的发送前回调服务,消息先回调给配置的回调服务器,然后去判断收发双方是否是好友关系,如果是好友关系,那么下发消息,如果是非好友关系,则不下发消息,客户端ui可以根据不下发返回的code做提示。


    57、调rest接口报401是什么原因?
    A:调环信rest接口,需要管理员权限的token,确认下请求是否有token,且是在有效期,token的有效期以请求时服务器返回的时间为准。


    58、调修改群信息报错如下
    System.Net.WebException:“远程服务器返回错误: (400) 错误的请求。
    A:检查下请求体,看下参数格式是否正确,比如"membersonly",,"allowinvites" 这两个参数的值为布尔值。


    59、注册用户username是纯数字可以吗。

    调restapi是可以的,serversdk的话,为了让用户使用更规范的名字,命名规则更严格一些,要求首位是字母。


    60、 SDK相关
    第1个 SDK 3.8.4 会有长链接特殊情况下无故断开情况,升级至3.8.5即可
    第2个 SDK3.8.5.1 会有重复收到推送的情况,升级到3.8.5.2即可
    第3个 SDK 3.8.2 以下启动闪退,报network问题,升级至 3.8.3.1即可


    61、 3.8.0以上版本与3.8.0以下版本有什么区别?

    目前官方demo最新版SDK版本号是3.8.6.2,SDK名称叫HyphenateChat
    3.8.0以下(不包含3.8.0)名称为Hyphenate
    如果您需要easeIMkit源码,建议您直接使用最新版.

    Hyphenate和HyphenateChat的关系:
    1.Hyphenate和HyphenateChat都是环信SDK,只需要引用一份即可.
    2.Hyphenate和HyphenateChat名字不同,版本也不同:
    Hyphenate是3.7.4及以下的SDK命名
    HyphenateChat是3.8.0及以上的SDK命名

    3.升级前后最大的区别:
    Hyphenate包含环信音视频功能
    配套的EaseIMKit(3.7.4/3.7.3两个版本)内部也有音视频的UI界面

    HyphenateChat已去除环信音视频,单有IM功能
    需要集成声网SDK
    配套EaseIMKit本身也没有音视频界面.
    EaseIMKit介绍文档:http://docs-im.easemob.com/im/ios/other/easeimkit#easeimkit_使用指南
    对应声网的音视频SDK,我们专门做了对应的UI界面:EaseCallKit.对EaseIMKit没有音视频界面的一个补充.


    注意:
    1 EaseCallKit就像EaseIMKit一样,也需要集成,而不被EaseIMKit包含.
    2 集成EaseCallKit前,还需要集成声网SDK,您可以阅读开发文档来理解其中关系:http://docs-im.easemob.com/im/ios/other/easecallkit#简介


    62、 如何集成easeIMkit源码?
    建议客户升级到最新版本即可集成easeIMkit源码.

    但需要注意:如果集成easeIMkit源码,虽然可以看到源码实现,但不能修改,如果需要修改调整,则需要使用本地集成的方式.
    本地集成方式可借鉴官方demo集成方式(这里由于步骤比较繁琐,如果客户还是不会,会考虑远程操作)

    63、本地集成easeIMkit,并修改调整,那么后续是不是无法再升级了?
    是这样的,虽然后续无法升级,但未来您是需要做出自己的ui界面的,甚至未来可能会移除easeIMkit,所以请大胆修改调整吧.

    我们更希望能够看到三个阶段:
    第一阶段:您可以简单快捷地集成环信聊天,并实现聊天功能.同时可以在EaseIMKit.framework开放的接口或属性范围内,可做出适合项目需求的调整.
    第二阶段:您在使用EaseIMKit.framework时遇到了限制,有诸多需要调整的部分在动态库内部,您无法去做调整.这时,您可以集成EaseIMKit源码来达到您的目的,集成EaseIMKit源码后,您可以修改源码内部的代码,以满足您项目需求.
    第三阶段:在您项目发展到一定程度后,往往EaseIMKit无法再陪伴您继续向更高层次发展.在这时,您对于EaseIMKit源码的熟悉度也非常高了.其中有很多实现方式已经为您之前遇到的困惑做了更好的解答,再加上您早已按耐不住的灵感,重建出属于您项目专属的界面吧!如此更加契合您的产品需求,也更加容易维护.

    64、 rest相关
     环信服务器的聊天记录能清除吗 删除了用户。重新注册的用户id对上了 又把聊天记录拉下来了

    你删了环信id,聊天记录是不会删除的,这么设计的逻辑是因为每个客户的业务场景不同,如果客户误删了环信id,需要重新注册回来,并且需要看到历史聊天记录。如果你这边的业务,是不希望这种场景,你可以去定义注册环信IM的id规则,你用户注册你自己应用的username时,按你定义的规则去注册IM的id,也就是说你这边的username和环信的id不是同一个,环信这边是根据环信id保存历史记录的

     
    65、关于rest接口注册用户,批量注册

    单次批量注册上限为100,不受接口限流频率影响.


    66、解决方案
    消息部分

    1.当前场景:SDK本身已经创建数据库,并有简单的增删改查,但由于SDK本身对于数据库的操作较为简单,所以舍弃当前SDK内数据库,创建新的数据库.

    举例:当前会搜索到携带关键字的所有类型的消息.期望只搜索我想搜索的消息类型

    思路:期望从根源上解决此类问题,让客户可以根据自己的业务场景来实现各类增删改查,突破SDK接口功能比较通用,却又无法主动实现复杂业务逻辑的痛点.


    67、当前场景:SDK文件存储限制. 
    思路:对接第三方文件存储。当前已可以提供上传下载思路与demo,需要的话官方支持群里@杨剑



    68、在聊天过程中,返回界面,未读消息数不归0问题
    一般未读消息数无法归零,最有可能的一个原因是easeIMkitmanager没有初始化,从而导致代理为nil,也就没有进行下一步的逻辑.可先检查下easeIMkitmanager是否进行初始化了.
    如果您这边没用到easeIMkit,可使用下面截图部分api进行操作:


    69、使用xcframework动态库时,如何去除动态库内的x86架构?
    xcframework不需要去除x86架构,直接打包即可.

    70、 3.8.4版本的gif大表情无法正常显示
    请升级版本至3.8.6以上

    71、 环信im针对appkey迁移用户数据
    我们无法操作客户数据,否则会破坏完整性,建议您自己操作

    72、在进入聊天界面时,会有一个滚动的动态效果,我不想要这个效果.
    您好,您这边将如下图所示红色箭头指向的bool值改成false即可



    73、服务端获取新的token之后旧的token是否失效?
    不会失效,token失效以服务器返回时间为准。


    74、客户APP集成环信后打包上架过程中审核报错:调用了苹果私有Api.

    1.尝试再次提交审核;2.发邮件申诉并贴出EMMessage代码。


    75、iOS端SDK没有找回密码的API,这个功能如何实现?

    需要后台调用Rest Api重置用户密码的接口即可。


    76、每次重启App,deviceToken会不会改变?

    不会改变,一般是卸载app重装,升级手机系统,deviceToken才会改变。


    77、如何获取昵称?
    环信是不涉及用户个人信息的,所以通过环信ID是获取不到用户昵称头像的,用户的个人信息可以在自己服务器与环信ID绑定存储维护,知道环信ID就可以到自己服务器下载这个环信ID对应的用户信息。

    78、线上的离线推送不成功,开发环境没问题,如何排查?
    换一个新的账号

    79、用户未读消息数,在服务端怎么获取?
    目前服务端不支持获取未读消息数,可在客户端获取。

    80、群组功能,邀请用户进入群组,被邀人能直接进入群组吗?还是需要被邀请人同意才能进入群组?
    客户端用户接收群邀请可以自动进入群组,也可以被邀请人同意后进入群组。

    81、如何同时集成IM和客服云SDK?
    开通IM和客服之后,只集成客服即可(客服内部有IM模块,可直接使用此模块)


    82、环信IM账号客户端退出登录为什么还能够收到推送消息?
    在登出的方法里将BOOL值设置为YES

    83、更换AppKey后服务端发送推送消息移动端收到的消息通知不显示消息内容?

    更换AppKey,下属账号默认不显示消息内容,客户端的默认设置不显示消息内容,需要将displayStyle打开即可。

    84、将用户拉入黑名单还能聊天?
    A把B拉黑,A能B发消息,但是B不能给A发消息。


    收起阅读 »

    旧版音视频与EaseCallKit兼容升级方案

    适用场景当前旧App(1.0)使用旧版音视频SDK,想升级到App 2.0,使用EaseCallKit,但不能强制客户的App升级,在一定时间内,App2.0要与App1.0同时存在,且可以进行音视频通信。方案一1、在App2.0中同时集成旧版音视频SDK(H...
    继续阅读 »

    适用场景
    当前旧App(1.0)使用旧版音视频SDK,想升级到App 2.0,使用EaseCallKit,但不能强制客户的App升级,在一定时间内,App2.0要与App1.0同时存在,且可以进行音视频通信。

    方案一

    1、在App2.0中同时集成旧版音视频SDK(Hyphenate)、声网SDK和EaseCallKit(EaseCallKit需要修改源码,改成使用Hyphenate),新增App Server(或已有AppServer),AppServer包含以下接口

    设置版本信息
    获取版本信息


    2、新App初始化时,调用AppServer,设置本身账户的版本信息 

    3、旧App呼叫,依然走旧版呼叫过程,新App集成了旧版音视频SDK,可以接通

    4、新App呼叫前,调用AppServer,获取被叫用户的版本信息,能获取到,则使用EaseCallKit呼叫,不能获取到版本信息,则依然走旧版SDK 呼叫过程




    问题:
    1、被叫方在多端同时登录,且各端的新旧版本不同,那么只有新版本能收到呼叫(多端不考虑。多端是指web端和移动端,不含桌面端)。
    2、客户先登录新版本,然后退出,再登录旧版本,无法接听(升级后再降级,不考虑)。

    方案二

    App 2.0 同时集成旧版本和新版本,初始化时从AppServer获取开关,若为关,使用旧版音视频,若为开,使用新版音视频,在3个月-6个月内开关关闭 ,大部分客户都更新新版本后,打开开关

    方案一和方案二可以结合使用

    收起阅读 »

    集成环信IM自定义添加表情组

    除了默认的兔斯基示例,想要自定义添加表情组,如何下手呢。今天手把手教你去哪里研究。1、iOS端添加自定义表情组先集成源码,然后找如下截图部分代码,这部分代码即为表情功能的逻辑,大家可以从此处着手,去实现自己需要的逻辑。2、Android端添加自定义表情组参考下...
    继续阅读 »

    除了默认的兔斯基示例,想要自定义添加表情组,如何下手呢。今天手把手教你去哪里研究。


    1、iOS端添加自定义表情组

    先集成源码,然后找如下截图部分代码,这部分代码即为表情功能的逻辑,大家可以从此处着手,去实现自己需要的逻辑。





    2、Android端添加自定义表情组

    参考下面这块代码添加表情组⬇️⬇️⬇️



    初始化之后设置这个provider,根据表情id返回具体表情数据



    其他的大家自己研究啦,如果还是没研究明白,欢迎留言~~

    收起阅读 »

    Android运行时权限终极方案,用PermissionX

    痛点在哪里?没有人愿意编写处理 Android 运行时权限的代码,因为它真的太繁琐了。这是一项没有什么技术含量,但是你又不得不去处理的工作,因为不处理它程序就会崩溃。但如果处理起来比较简单也就算了,可事实上,Android 提供给我们的运行时权限 API 并不...
    继续阅读 »

    痛点在哪里?

    没有人愿意编写处理 Android 运行时权限的代码,因为它真的太繁琐了。

    这是一项没有什么技术含量,但是你又不得不去处理的工作,因为不处理它程序就会崩溃。但如果处理起来比较简单也就算了,可事实上,Android 提供给我们的运行时权限 API 并不友好。

    以一个拨打电话的功能为例,因为 CALL_PHONE 权限是危险权限,所以在我们除了要在 AndroidManifest.xml 中声明权限之外,还要在执行拨打电话操作之前进行运行时权限处理才行。

    权限声明如下:

    然后,编写如下代码来进行运行时权限处理

    class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    makeCallBtn.setOnClickListener {
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_GRANTED) {
    call()
    } else {
    ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CALL_PHONE), 1)
    }
    }
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    when (requestCode) {
    1 -> {
    if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
    call()
    } else {
    Toast.makeText(this, "You denied CALL_PHONE permission", Toast.LENGTH_SHORT).show()
    }
    }
    }
    }

    private fun call() {
    try {
    val intent = Intent(Intent.ACTION_CALL)
    intent.data = Uri.parse("tel:10086")
    startActivity(intent)
    } catch (e: SecurityException) {
    e.printStackTrace()
    }
    }

    }


    这段代码中真有正意义的功能逻辑就是 call() 方法中的内容,可是如果直接调用 call() 方法是无法实现拨打电话功能的,因为我们还没有申请 CALL_PHONE 权限。

    那么整段代码其他的部分就都是在处理 CALL_PHONE 权限申请。可以看到,这里需要先判断用户是否已授权我们拨打电话的权限,如果没有的话则要进行权限申请,然后还要在 onRequestPermissionsResult() 回调中处理权限申请的结果,最后才能去执行拨打电话的操作。

    你可能觉得,这也不算是很繁琐呀,代码量并不是很多。那是因为,目前我们还只是处理了运行时权限最简单的场景,而实际的项目环境中有着更加复杂的场景在等着我们。

    比如说,你的 App 可能并不只是单单申请一个权限,而是需要同时申请多个权限。虽然 ActivityCompat.requestPermissions() 方法允许一次性传入多个权限名,但是你在 onRequestPermissionsResult() 回调中就需要判断哪些权限被允许了,哪些权限被拒绝了,被拒绝的权限是否影响到应用程序的核心功能,以及是否要再次申请权限。

    而一旦牵扯到再次申请权限,就引出了一个更加复杂的问题。你申请的权限被用户拒绝过了一次,那么再次申请将很有可能再次被拒绝。为此,Android 提供了一个 shouldShowRequestPermissionRationale() 方法,用于判断是否需要向用户解释申请这个权限的原因,一旦 shouldShowRequestPermissionRationale() 方法返回 true,那么我们最好弹出一个对话框来向用户阐明为什么我们是需要这个权限的,这样可以增加用户同意授权的几率。

    是不是已经觉得很复杂了?不过还没完,Android 系统还提供了一个 “拒绝,不要再询问” 的选项,如下图所示:

    只要用户选择了这个选项,那么我们以后每次执行权限申请的代码都将会直接被拒绝。

    可是如果我的某项功能就是必须要依赖这个权限才行呢?没有办法,你只能提示用户去应用程序设置当中手动打开权限,程序方面已无法进行操作。

    可以看出,如果想要在项目中对运行时权限做出非常全面的处理,是一件相当复杂的事情。事实上,大部分的项目都没有将权限申请这块处理得十分恰当,这也是我编写 PermissionX 的理由。

    PermissionX 的实现原理

    在开始介绍 PermissionX 的具体用法之前,我们先来讨论一下它的实现原理。

    其实之前并不是没有人尝试过对运行时权限处理进行封装,我之前在做直播公开课的时候也向大家演示过一种运行时权限 API 的封装过程。

    但是,想要对运行时权限的 API 进行封装并不是一件容易的事,因为这个操作是有特定的上下文依赖的,一般需要在 Activity 中接收 onRequestPermissionsResult() 方法的回调才行,所以不能简单地将整个操作封装到一个独立的类中。

    为此,也衍生出了一系列特殊的封装方案,比如将运行时权限的操作封装到 BaseActivity 中,或者提供一个透明的 Activity 来处理运行时权限等。

    不过上述两种方案都不够轻量,因为改变 Activity 的继承结构这可是大事情,而提供一个透明的 Activty 则需要在 AndroidManifest.xml 中进行额外的声明。

    现在,业内普遍比较认可使用另外一种小技巧来进行实现。是什么小技巧呢?回想一下,之前所有申请运行时权限的操作都是在 Activity 中进行的,事实上,Android 在 Fragment 中也提供了一份相同的 API,使得我们在 Fragment 中也能申请运行时权限。

    但不同的是,Fragment 并不像 Activity 那样必须有界面,我们完全可以向 Activity 中添加一个隐藏的 Fragment,然后在这个隐藏的 Fragment 中对运行时权限的 API 进行封装。这是一种非常轻量级的做法,不用担心隐藏 Fragment 会对 Activity 的性能造成什么影响。

    这就是 PermissionX 的实现原理了,书中其实也已经介绍过了这部分内容。但是,在其实现原理的基础之上,后期我又增加了很多新功能,让 PermissionX 变得更加强大和好用,下面我们就来学习一下 PermissionX 的具体用法。

    基本用法

    要使用 PermissionX 之前,首先需要将其引入到项目当中,如下所示

    dependencies {
    ...
    implementation 'com.permissionx.guolindev:permissionx:1.1.1'
    }


    我在写本篇文章时 PermissionX 的最新版本是 1.1.1,想要查看它的当前最新版本,请访问 PermissionX 的主页:github.com/guolindev/P…

    PermissionX 的目的是为了让运行时权限处理尽可能的容易,因此怎么让 API 变得简单好用就是我优先要考虑的问题。

    比如同样实现拨打电话的功能,使用 PermissionX 只需要这样写:

    class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    makeCallBtn.setOnClickListener {
    PermissionX.init(this)
    .permissions(Manifest.permission.CALL_PHONE)
    .request { allGranted, grantedList, deniedList ->
    if (allGranted) {
    call()
    } else {
    Toast.makeText(this, "您拒绝了拨打电话权限", Toast.LENGTH_SHORT).show()
    }
    }
    }
    }

    ...

    }


    是的,PermissionX 的基本用法就这么简单。首先调用 init() 方法来进行初始化,并在初始化的时候传入一个 FragmentActivity 参数。由于 AppCompatActivity 是 FragmentActivity 的子类,所以只要你的 Activity 是继承自 AppCompatActivity 的,那么直接传入 this 就可以了。

    接下来调用 permissions() 方法传入你要申请的权限名,这里传入 CALL_PHONE 权限。你也可以在 permissions() 方法中传入任意多个权限名,中间用逗号隔开即可。

    最后调用 request() 方法来执行权限申请,并在 Lambda 表达式中处理申请结果。可以看到,Lambda 表达式中有 3 个参数:allGranted 表示是否所有申请的权限都已被授权,grantedList 用于记录所有已被授权的权限,deniedList 用于记录所有被拒绝的权限。

    因为我们只申请了一个 CALL_PHONE 权限,因此这里直接判断:如果 allGranted 为 true,那么就调用 call() 方法,否则弹出一个 Toast 提示。

    运行结果如下:

    怎么样?对比之前的写法,是不是觉得运行时权限处理没那么繁琐了?

    核心用法

    然而我们目前还只是处理了最普通的场景,刚才提到的,假如用户拒绝了某个权限,在下次申请之前,我们最好弹出一个对话框来向用户解释申请这个权限的原因,这个又该怎么实现呢?

    别担心,PermissionX 对这些情况进行了充分的考虑。

    onExplainRequestReason() 方法可以用于监听那些被用户拒绝,而又可以再次去申请的权限。从方法名上也可以看出来了,应该在这个方法中解释申请这些权限的原因。

    而我们只需要将 onExplainRequestReason() 方法串接到 request() 方法之前即可,如下所示:

    PermissionX.init(this)
    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
    .onExplainRequestReason { deniedList ->
    }
    .request { allGranted, grantedList, deniedList ->
    if (allGranted) {
    Toast.makeText(this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
    } else {
    Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
    }
    }


    这种情况下,所有被用户拒绝的权限会优先进入 onExplainRequestReason() 方法进行处理,拒绝的权限都记录在 deniedList 参数当中。接下来,我们只需要在这个方法中调用 showRequestReasonDialog() 方法,即可弹出解释权限申请原因的对话框,如下所示:

    PermissionX.init(this)
    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
    .onExplainRequestReason { deniedList ->
    showRequestReasonDialog(deniedList, "即将重新申请的权限是程序必须依赖的权限", "我已明白", "取消")
    }
    .request { allGranted, grantedList, deniedList ->
    if (allGranted) {
    Toast.makeText(this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
    } else {
    Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
    }
    }


    showRequestReasonDialog() 方法接受 4 个参数:第一个参数是要重新申请的权限列表,这里直接将 deniedList 参数传入。第二个参数则是要向用户解释的原因,我只是随便写了一句话,这个参数描述的越详细越好。第三个参数是对话框上确定按钮的文字,点击该按钮后将会重新执行权限申请操作。第四个参数是一个可选参数,如果不传的话相当于用户必须同意申请的这些权限,否则对话框无法关闭,而如果传入的话,对话框上会有一个取消按钮,点击取消后不会重新进行权限申请,而是会把当前的申请结果回调到 request() 方法当中。

    另外始终要记得将所有申请的权限都在 AndroidManifest.xml 中进行声明:

    重新运行一下程序,效果如下图所示:

    当前版本解释权限申请原因对话框的样式还无法自定义,1.3.0 版本当中已支持了自定义权限提醒对话框样式的功能,详情请参阅 PermissionX 重磅更新,支持自定义权限提醒对话框 。

    当然,我们也可以指定要对哪些权限重新申请,比如上述申请的 3 个权限中,我认为 CAMERA 权限是必不可少的,而其他两个权限则可有可无,那么在重新申请的时候也可以只申请 CAMERA 权限:


    PermissionX.init(this)   
    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.ACCESS_FINE_LOCATION)
    .onExplainRequestReason { deniedList ->
    val filteredList = deniedList.filter {
    it == Manifest.permission.CAMERA
    }
    showRequestReasonDialog(filteredList, "摄像机权限是程序必须依赖的权限", "我已明白", "取消")
    }
    .request { allGranted, grantedList, deniedList ->
    if (allGranted) {
    Toast.makeText(this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
    } else {
    Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
    }
    }


    这样当再次申请权限的时候就只会申请 CAMERA 权限,剩下的两个权限最终会被传入到 request() 方法的 deniedList 参数当中。

    解决了向用户解释权限申请原因的问题,接下来还有一个头疼的问题要解决:如果用户不理会我们的解释,仍然执意拒绝权限申请,并且还选择了拒绝且不再询问的选项,这该怎么办?通常这种情况下,程序层面已经无法再次做出权限申请,唯一能做的就是提示用户到应用程序设置当中手动打开权限。

    更多用法

    那么 PermissionX 是如何处理这种情况的呢?我相信绝对会给你带来惊喜。PermissionX 中还提供了一个 onForwardToSettings() 方法,专门用于监听那些被用户永久拒绝的权限。另外从方法名上就可以看出,我们可以在这里提醒用户手动去应用程序设置当中打开权限。代码如下所示:

    PermissionX.init(this)
    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
    .onExplainRequestReason { deniedList ->
    showRequestReasonDialog(deniedList, "即将重新申请的权限是程序必须依赖的权限", "我已明白", "取消")
    }
    .onForwardToSettings { deniedList ->
    showForwardToSettingsDialog(deniedList, "您需要去应用程序设置当中手动开启权限", "我已明白", "取消")
    }
    .request { allGranted, grantedList, deniedList ->
    if (allGranted) {
    Toast.makeText(this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
    } else {
    Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
    }
    }


    可以看到,这里又串接了一个 onForwardToSettings() 方法,所有被用户选择了拒绝且不再询问的权限都会进行到这个方法中处理,拒绝的权限都记录在 deniedList 参数当中。

    接下来,你并不需要自己弹出一个 Toast 或是对话框来提醒用户手动去应用程序设置当中打开权限,而是直接调用 showForwardToSettingsDialog() 方法即可。类似地,showForwardToSettingsDialog() 方法也接收 4 个参数,每个参数的作用和刚才的 showRequestReasonDialog() 方法完全一致,我这里就不再重复解释了。

    showForwardToSettingsDialog() 方法将会弹出一个对话框,当用户点击对话框上的我已明白按钮时,将会自动跳转到当前应用程序的设置界面,从而不需要用户自己慢慢进入设置当中寻找当前应用了。另外,当用户从设置中返回时,PermissionX 将会自动重新请求相应的权限,并将最终的授权结果回调到 request() 方法当中。效果如下图所示:

    同样,1.3.0 版本也支持了自定义这个对话框样式的功能,详情请参阅 PermissionX 重磅更新,支持自定义权限提醒对话框 。

    PermissionX 最主要的功能大概就是这些,不过我在使用一些 App 的时候发现,有些 App 喜欢在第一次请求权限之前就先弹出一个对话框向用户解释自己需要哪些权限,然后才会进行权限申请。这种做法是比较提倡的,因为用户同意授权的概率会更高。

    那么 PermissionX 中要如何实现这样的功能呢?

    其实非常简单,PermissionX 还提供了一个 explainReasonBeforeRequest() 方法,只需要将它也串接到 request() 方法之前就可以了,代码如下所示:

    PermissionX.init(this)
    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
    .explainReasonBeforeRequest()
    .onExplainRequestReason { deniedList ->
    showRequestReasonDialog(deniedList, "即将申请的权限是程序必须依赖的权限", "我已明白")
    }
    .onForwardToSettings { deniedList ->
    showForwardToSettingsDialog(deniedList, "您需要去应用程序设置当中手动开启权限", "我已明白")
    }
    .request { allGranted, grantedList, deniedList ->
    if (allGranted) {
    Toast.makeText(this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
    } else {
    Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
    }
    }


    这样,当每次请求权限时,会优先进入 onExplainRequestReason() 方法,弹出解释权限申请原因的对话框,用户点击我已明白按钮之后才会执行权限申请。效果如下图所示:

    不过,你在使用 explainReasonBeforeRequest() 方法时,其实还有一些关键的点需要注意。

    第一,单独使用 explainReasonBeforeRequest() 方法是无效的,必须配合 onExplainRequestReason() 方法一起使用才能起作用。这个很好理解,因为没有配置 onExplainRequestReason() 方法,我们怎么向用户解释权限申请原因呢?

    第二,在使用 explainReasonBeforeRequest() 方法时,如果 onExplainRequestReason() 方法中编写了权限过滤的逻辑,最终的运行结果可能和你期望的会不一致。这一点可能会稍微有点难理解,我用一个具体的示例来解释一下。

    观察如下代码:

    PermissionX.init(this)
    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
    .explainReasonBeforeRequest()
    .onExplainRequestReason { deniedList ->
    val filteredList = deniedList.filter {
    it == Manifest.permission.CAMERA
    }
    showRequestReasonDialog(filteredList, "摄像机权限是程序必须依赖的权限", "我已明白")
    }
    ...


    这里在 onExplainRequestReason() 方法中编写了刚才用到的权限过滤逻辑,当有多个权限被拒绝时,我们只重新申请 CAMERA 权限。

    在没有加入 explainReasonBeforeRequest() 方法时,一切都可以按照我们所预期的那样正常运行。但如果加上了 explainReasonBeforeRequest() 方法,在执行权限请求之前会先进入 onExplainRequestReason() 方法,而这里将除了 CAMERA 之外的其他权限都过滤掉了,因此实际上 PermissionX 只会请求 CAMERA 这一个权限,剩下的权限将完全不会尝试去请求,而是直接作为被拒绝的权限回调到最终的 request() 方法当中。

    效果如下图所示:

    针对于这种情况,PermissionX 在 onExplainRequestReason() 方法中提供了一个额外的 beforeRequest 参数,用于标识当前上下文是在权限请求之前还是之后,借助这个参数在 onExplainRequestReason() 方法中执行不同的逻辑,即可很好地解决这个问题,示例代码如下:

    PermissionX.init(this)
    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
    .explainReasonBeforeRequest()
    .onExplainRequestReason { deniedList, beforeRequest ->
    if (beforeRequest) {
    showRequestReasonDialog(deniedList, "为了保证程序正常工作,请您同意以下权限申请", "我已明白")
    } else {
    val filteredList = deniedList.filter {
    it == Manifest.permission.CAMERA
    }
    showRequestReasonDialog(filteredList, "摄像机权限是程序必须依赖的权限", "我已明白")
    }
    }
    ...


    可以看到,当 beforeRequest 为 true 时,说明此时还未执行权限申请,那么我们将完整的 deniedList 传入 showRequestReasonDialog() 方法当中。

    而当 beforeRequest 为 false 时,说明某些权限被用户拒绝了,此时我们只重新申请 CAMERA 权限,因为它是必不可少的,其他权限则可有可无。

    最终运行效果如下:

    代码下载:XPermission-master.zip

    收起阅读 »

    Android自定义View 雷达扫描效果

    最近在做一个项目,其中有一个页面是要做一个类似于雷达扫描的效果。于是找了其他应用的类似的效果参考一下,刚好我使用的华为手机里的手机管家--病毒查杀页面就是一个雷达扫描的效果。而且看它的样式也挺不错的,刚好符合我的要求。所以就决定仿照它的样式自定义一个类似效果的...
    继续阅读 »

    最近在做一个项目,其中有一个页面是要做一个类似于雷达扫描的效果。于是找了其他应用的类似的效果参考一下,刚好我使用的华为手机里的手机管家--病毒查杀页面就是一个雷达扫描的效果。而且看它的样式也挺不错的,刚好符合我的要求。所以就决定仿照它的样式自定义一个类似效果的RadarView。 这是华为手机管家的效果:

    图片
    我写完这个RadarView之后觉得这个View的实现虽然不难,却使用到了自定义属性、View的Measure、Paint、Canvas和坐标的计算等这些自定义View常用的知识,是一个不错的自定义View练习例子,所以决定写一篇博客把它记录起来。

    由于我需要雷达的扫描效果,所以画中间的百分比数字。RadarView可以根据自己的需求配置View的主题颜色、扫描颜色、扫描速度、圆圈数量、是否显示水滴等功能样式,方便实现各种样式的情况。下面是自定义RadarView的代码。

    public class RadarView extends View {

    //默认的主题颜色
    private int DEFAULT_COLOR = Color.parseColor("#91D7F4");

    // 圆圈和交叉线的颜色
    private int mCircleColor = DEFAULT_COLOR;
    //圆圈的数量 不能小于1
    private int mCircleNum = 3;
    //扫描的颜色 RadarView会对这个颜色做渐变透明处理
    private int mSweepColor = DEFAULT_COLOR;
    //水滴的颜色
    private int mRaindropColor = DEFAULT_COLOR;
    //水滴的数量 这里表示的是水滴最多能同时出现的数量。因为水滴是随机产生的,数量是不确定的
    private int mRaindropNum = 4;
    //是否显示交叉线
    private boolean isShowCross = true;
    //是否显示水滴
    private boolean isShowRaindrop = true;
    //扫描的转速,表示几秒转一圈
    private float mSpeed = 3.0f;
    //水滴显示和消失的速度
    private float mFlicker = 3.0f;

    private Paint mCirclePaint;// 圆的画笔
    private Paint mSweepPaint; //扫描效果的画笔
    private Paint mRaindropPaint;// 水滴的画笔

    private float mDegrees; //扫描时的扫描旋转角度。
    private boolean isScanning = false;//是否扫描

    //保存水滴数据
    private ArrayList mRaindrops = new ArrayList<>();

    public RadarView(Context context) {
    super(context);
    init();
    }

    public RadarView(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
    getAttrs(context, attrs);
    init();
    }

    public RadarView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    getAttrs(context, attrs);
    init();
    }

    /**
    * 获取自定义属性值
    *
    * @param context
    * @param attrs
    */

    private void getAttrs(Context context, AttributeSet attrs) {
    if (attrs != null) {
    TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.RadarView);
    mCircleColor = mTypedArray.getColor(R.styleable.RadarView_circleColor, DEFAULT_COLOR);
    mCircleNum = mTypedArray.getInt(R.styleable.RadarView_circleNum, mCircleNum);
    if (mCircleNum < 1) {
    mCircleNum = 3;
    }
    mSweepColor = mTypedArray.getColor(R.styleable.RadarView_sweepColor, DEFAULT_COLOR);
    mRaindropColor = mTypedArray.getColor(R.styleable.RadarView_raindropColor, DEFAULT_COLOR);
    mRaindropNum = mTypedArray.getInt(R.styleable.RadarView_raindropNum, mRaindropNum);
    isShowCross = mTypedArray.getBoolean(R.styleable.RadarView_showCross, true);
    isShowRaindrop = mTypedArray.getBoolean(R.styleable.RadarView_showRaindrop, true);
    mSpeed = mTypedArray.getFloat(R.styleable.RadarView_speed, mSpeed);
    if (mSpeed <= 0) {
    mSpeed = 3;
    }
    mFlicker = mTypedArray.getFloat(R.styleable.RadarView_flicker, mFlicker);
    if (mFlicker <= 0) {
    mFlicker = 3;
    }
    mTypedArray.recycle();
    }
    }

    /**
    * 初始化
    */

    private void init() {
    // 初始化画笔
    mCirclePaint = new Paint();
    mCirclePaint.setColor(mCircleColor);
    mCirclePaint.setStrokeWidth(1);
    mCirclePaint.setStyle(Paint.Style.STROKE);
    mCirclePaint.setAntiAlias(true);

    mRaindropPaint = new Paint();
    mRaindropPaint.setStyle(Paint.Style.FILL);
    mRaindropPaint.setAntiAlias(true);

    mSweepPaint = new Paint();
    mSweepPaint.setAntiAlias(true);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //设置宽高,默认200dp
    int defaultSize = dp2px(getContext(), 200);
    setMeasuredDimension(measureWidth(widthMeasureSpec, defaultSize),
    measureHeight(heightMeasureSpec, defaultSize));
    }

    /**
    * 测量宽
    *
    * @param measureSpec
    * @param defaultSize
    * @return
    */

    private int measureWidth(int measureSpec, int defaultSize) {
    int result = 0;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    if (specMode == MeasureSpec.EXACTLY) {
    result = specSize;
    } else {
    result = defaultSize + getPaddingLeft() + getPaddingRight();
    if (specMode == MeasureSpec.AT_MOST) {
    result = Math.min(result, specSize);
    }
    }
    result = Math.max(result, getSuggestedMinimumWidth());
    return result;
    }

    /**
    * 测量高
    *
    * @param measureSpec
    * @param defaultSize
    * @return
    */

    private int measureHeight(int measureSpec, int defaultSize) {
    int result = 0;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    if (specMode == MeasureSpec.EXACTLY) {
    result = specSize;
    } else {
    result = defaultSize + getPaddingTop() + getPaddingBottom();
    if (specMode == MeasureSpec.AT_MOST) {
    result = Math.min(result, specSize);
    }
    }
    result = Math.max(result, getSuggestedMinimumHeight());
    return result;
    }

    @Override
    protected void onDraw(Canvas canvas) {

    //计算圆的半径
    int width = getWidth() - getPaddingLeft() - getPaddingRight();
    int height = getHeight() - getPaddingTop() - getPaddingBottom();
    int radius = Math.min(width, height) / 2;

    //计算圆的圆心
    int cx = getPaddingLeft() + (getWidth() - getPaddingLeft() - getPaddingRight()) / 2;
    int cy = getPaddingTop() + (getHeight() - getPaddingTop() - getPaddingBottom()) / 2;

    drawCircle(canvas, cx, cy, radius);

    if (isShowCross) {
    drawCross(canvas, cx, cy, radius);
    }

    //正在扫描
    if (isScanning) {
    if (isShowRaindrop) {
    drawRaindrop(canvas, cx, cy, radius);
    }
    drawSweep(canvas, cx, cy, radius);
    //计算雷达扫描的旋转角度
    mDegrees = (mDegrees + (360 / mSpeed / 60)) % 360;

    //触发View重新绘制,通过不断的绘制实现View的扫描动画效果
    invalidate();
    }
    }

    /**
    * 画圆
    */

    private void drawCircle(Canvas canvas, int cx, int cy, int radius) {
    //画mCircleNum个半径不等的圆圈。
    for (int i = 0; i < mCircleNum; i++) {
    canvas.drawCircle(cx, cy, radius - (radius / mCircleNum * i), mCirclePaint);
    }
    }

    /**
    * 画交叉线
    */

    private void drawCross(Canvas canvas, int cx, int cy, int radius) {
    //水平线
    canvas.drawLine(cx - radius, cy, cx + radius, cy, mCirclePaint);

    //垂直线
    canvas.drawLine(cx, cy - radius, cx, cy + radius, mCirclePaint);
    }

    /**
    * 生成水滴。水滴的生成是随机的,并不是每次调用都会生成一个水滴。
    */

    private void generateRaindrop(int cx, int cy, int radius) {

    // 最多只能同时存在mRaindropNum个水滴。
    if (mRaindrops.size() < mRaindropNum) {
    // 随机一个20以内的数字,如果这个数字刚好是0,就生成一个水滴。
    // 用于控制水滴生成的概率。
    boolean probability = (int) (Math.random() * 20) == 0;
    if (probability) {
    int x = 0;
    int y = 0;
    int xOffset = (int) (Math.random() * (radius - 20));
    int yOffset = (int) (Math.random() * (int) Math.sqrt(1.0 * (radius - 20) * (radius - 20) - xOffset * xOffset));

    if ((int) (Math.random() * 2) == 0) {
    x = cx - xOffset;
    } else {
    x = cx + xOffset;
    }

    if ((int) (Math.random() * 2) == 0) {
    y = cy - yOffset;
    } else {
    y = cy + yOffset;
    }

    mRaindrops.add(new Raindrop(x, y, 0, mRaindropColor));
    }
    }
    }

    /**
    * 删除水滴
    */

    private void removeRaindrop() {
    Iterator iterator = mRaindrops.iterator();

    while (iterator.hasNext()) {
    Raindrop raindrop = iterator.next();
    if (raindrop.radius > 20 || raindrop.alpha < 0) {
    iterator.remove();
    }
    }
    }

    /**
    * 画雨点(就是在扫描的过程中随机出现的点)。
    */

    private void drawRaindrop(Canvas canvas, int cx, int cy, int radius) {
    generateRaindrop(cx, cy, radius);
    for (Raindrop raindrop : mRaindrops) {
    mRaindropPaint.setColor(raindrop.changeAlpha());
    canvas.drawCircle(raindrop.x, raindrop.y, raindrop.radius, mRaindropPaint);
    //水滴的扩散和透明的渐变效果
    raindrop.radius += 1.0f * 20 / 60 / mFlicker;
    raindrop.alpha -= 1.0f * 255 / 60 / mFlicker;
    }
    removeRaindrop();
    }

    /**
    * 画扫描效果
    */

    private void drawSweep(Canvas canvas, int cx, int cy, int radius) {
    //扇形的透明的渐变效果
    SweepGradient sweepGradient = new SweepGradient(cx, cy,
    new int[]{Color.TRANSPARENT, changeAlpha(mSweepColor, 0), changeAlpha(mSweepColor, 168),
    changeAlpha(mSweepColor, 255), changeAlpha(mSweepColor, 255)
    }, new float[]{0.0f, 0.6f, 0.99f, 0.998f, 1f});
    mSweepPaint.setShader(sweepGradient);
    //先旋转画布,再绘制扫描的颜色渲染,实现扫描时的旋转效果。
    canvas.rotate(-90 + mDegrees, cx, cy);
    canvas.drawCircle(cx, cy, radius, mSweepPaint);
    }

    /**
    * 开始扫描
    */

    public void start() {
    if (!isScanning) {
    isScanning = true;
    invalidate();
    }
    }

    /**
    * 停止扫描
    */

    public void stop() {
    if (isScanning) {
    isScanning = false;
    mRaindrops.clear();
    mDegrees = 0.0f;
    }
    }

    /**
    * 水滴数据类
    */

    private static class Raindrop {

    int x;
    int y;
    float radius;
    int color;
    float alpha = 255;

    public Raindrop(int x, int y, float radius, int color) {
    this.x = x;
    this.y = y;
    this.radius = radius;
    this.color = color;
    }

    /**
    * 获取改变透明度后的颜色值
    *
    * @return
    */

    public int changeAlpha() {
    return RadarView.changeAlpha(color, (int) alpha);
    }

    }

    /**
    * dp转px
    */

    private static int dp2px(Context context, float dpVal) {
    return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
    dpVal, context.getResources().getDisplayMetrics());
    }

    /**
    * 改变颜色的透明度
    *
    * @param color
    * @param alpha
    * @return
    */

    private static int changeAlpha(int color, int alpha) {
    int red = Color.red(color);
    int green = Color.green(color);
    int blue = Color.blue(color);
    return Color.argb(alpha, red, green, blue);
    }
    }

    自定义属性:在res/values下创建attrs.xml


























    效果图:

    效果图

    代码下载:mirrors-XHRadarView-master.zip
    收起阅读 »

    Android右侧边栏滚动选择

    Android右侧边栏滚动选择涉及到的内容:首先会ListView或RecyclerView的多布局。自定义View右侧拼音列表,简单地绘制并设立监听事件等。会使用pinyin4.jar第三方包来识别汉字的首字母(单独处理重庆多音问题)。将全部的城市列表转化为...
    继续阅读 »

    Android右侧边栏滚动选择

    涉及到的内容:

    1. 首先会ListView或RecyclerView的多布局。

    2. 自定义View右侧拼音列表,简单地绘制并设立监听事件等。

    3. 会使用pinyin4.jar第三方包来识别汉字的首字母(单独处理重庆多音问题)。

    4. 将全部的城市列表转化为{A a开头城市名...,B b开头城市名...}的格式,这个数据转化是重点 !!!

    5. 将第三步获取的数据来多布局展示出来。

    难点:

    1、RecyclerView的滑动问题

    2、RecyclerView的点击问题

    3、绘制SideBar

    先来看个图,看是不是你想要的

    1557800237747.gif

    实现思路

    根据城市和拼音列表,可以想到多布局,这里无非是把城市名称按其首字母进行排列后再填充列表,如果给你一组数据{A、城市1、城市2、B、城市3、城市4...}这样的数据让你填充你总会吧,无非就是两种布局,将拼音和汉字的背景设置不同就行;右侧是个自定义布局,别说你不会自定义布局,不会也行,这个很简单,无非是平分高度,通过drawText()绘制字母,然后进行滑动监听,右侧滑动或点击到哪里,左侧列表相应进行滚动即可。

    其实原先我已经通过ListView做过了,这次回顾使用RecyclerView再实现一次,发现还遇到了一些新东西,带你们看看。这次没有使用BaseQuickAdapter,使用多了都忘记原始的代码怎么敲了话不多说开撸吧

    1. 确定数据格式

    首先我们需要确定下Bean的数据格式,毕竟涉及到多布局

    public class ItemBean {

    private String itemName;//城市名或者字母A...
    private String itemType;//类型,区分是首字母还是城市名,是首字母的写“head”,不是的填入其它字母都行

    // 标记 拼音头,head为0
    public static final int TYPE_HEAD = 0;
    // 标记 城市名
    public static final int TYPE_CITY = 1;

    public int getType() {
    if (itemType.equals("head")) {
    return TYPE_HEAD;
    } else {
    return TYPE_CITY;
    }
    }
    ......Get Set方法
    }

    可以看到有两个字段,一个用来显示城市名或者字母,另一个用来区分是城市还是首字母。这里定义了个getType()方法,为字母的话返回0,城市名返回1

    2. 整理数据

    一般我们准备的数据都是这样的


    "mycityarray">
    北京市
    上海市
    广州市
    天津市
    石家庄市
    唐山市
    秦皇岛市
    邯郸市
    邢台市
    保定市
    张家口市
    承德市市
    沧州市
    廊坊市
    衡水市
    ......



    想要得到我们那样的数据,需要先获取这些城市名的首字母然后进行排序,这里我使用pinyin4j-2.5.0.jar进行汉字到拼音的转化,jar下载地址

    2.1 编写工具类

    public class HanziToPinYin {
    /**
    * 如果字符串string是汉字,则转为拼音并返回,返回的是首字母
    *
    @param string
    *
    @return
    */

    public static char toPinYin(String string){
    HanyuPinyinOutputFormat hanyuPinyin = new HanyuPinyinOutputFormat();
    hanyuPinyin.setCaseType(HanyuPinyinCaseType.UPPERCASE);
    hanyuPinyin.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
    hanyuPinyin.setVCharType(HanyuPinyinVCharType.WITH_U_UNICODE);
    String[] pinyinArray=null;
    char hanzi = string.charAt(0);
    try {
    //是否在汉字范围内
    if(hanzi>=0x4e00 && hanzi<=0x9fa5){
    pinyinArray = PinyinHelper.toHanyuPinyinStringArray(hanzi, hanyuPinyin);
    }
    } catch (BadHanyuPinyinOutputFormatCombination e) {
    e.printStackTrace();
    }
    //将获取到的拼音返回,只返回其首字母
    return pinyinArray[0].charAt(0);
    }
    }

    2.2 整理数据

    private List cityList;      //给定的所有的城市名
    private List itemList; //整理后的所有的item子项,可能是城市、可能是字母

    //初始化数据,将所有城市进行排序,且加上字母和它们一起形成新的集合
    private void initData(){

    itemList = new ArrayList<>();
    //获取所有的城市名
    String[] cityArray = getResources().getStringArray(R.array.mycityarray);
    cityList = Arrays.asList(cityArray);
    //将所有城市进行排序,排完后cityList内所有的城市名都是按首字母进行排序的
    Collections.sort(cityList, new CityComparator());

    //将剩余的城市加进去
    for (int i = 0; i < cityList.size(); i++) {

    String city = cityList.get(i);
    String letter = null; //当前所属的字母

    if (city.contains("重庆")) {
    letter = HanziToPinYin.toPinYin("崇庆") + "";
    } else {
    letter = HanziToPinYin.toPinYin(cityList.get(i)) + "";
    }

    if (letter.equals(currentLetter)) { //在A字母下,属于当前字母
    itemBean = new ItemBean();
    itemBean.setItemName(city); //把汉字放进去
    itemBean.setItemType(letter); //这里放入其它不是“head”的字符串就行
    itemList.add(itemBean);
    } else { //不在当前字母下,先将该字母取出作为独立的一个item
    //添加标签(B...)
    itemBean = new ItemBean();
    itemBean.setItemName(letter); //把首字母进去
    itemBean.setItemType("head"); //把head标签放进去
    currentLetter = letter;
    itemList.add(itemBean);

    //添加城市
    itemBean = new ItemBean();
    itemBean.setItemName(city); //把汉字放进去
    itemBean.setItemType(letter); //把拼音放进去
    itemList.add(itemBean);
    }
    }
    }

    经过以上步骤就将原先的数据整理成了以下形式排列的一组数据

    {
    {itemName:"A",itemType:"head"}
    {itemName:"阿拉善盟",itemType:"A"}
    {itemName:"安抚市",itemType:"A"}
    ...
    {itemName:"巴中市",itemType:"B"}
    {itemName:"白山市",itemType:"B"}
    ....
    }

    等等,上面有个Collections.sort(cityList, new CityComparator());letter = HanziToPinYin.toPinYin("崇庆") + "";你可能还会有疑惑,我就来多几嘴 因为pinyin4j.jar这个jar包在将汉字转为拼音的时候,会将重庆的拼音转为zhongqin,所以在排序和获取首字母的时候都需要单独处理

    public class CityComparator implements Comparator<String> {

    private RuleBasedCollator collator;

    public CityComparator() {
    collator = (RuleBasedCollator) Collator.getInstance(Locale.CHINA);
    }

    @Override
    public int compare(String lhs, String rhs) {

    lhs = lhs.replace("重庆", "崇庆");
    rhs = rhs.replace("重庆", "崇庆");
    CollationKey c1 = collator.getCollationKey(lhs);
    CollationKey c2 = collator.getCollationKey(rhs);

    return c1.compareTo(c2);
    }
    }

    这里先指定RuleBasedCollator语言环境为CHINA,然后在compare()比较方法里,如果遇到两边有"重庆"的字符串,就将其替换为”崇庆“,然后通过getCollationKey()获取首个字符然后进行比较。

    letter = HanziToPinYin.toPinYin("崇庆") + "";获取首字母的时候也是同样,不是获取"重庆"的首字母而是"崇庆"的首字母。

    看到这样的一组数据你总会根据多布局来给RecyclerView填充数据了吧

    3. RecyclerView填充数据

    既然涉及到多布局,那么有几种布局就该有几个ViewHolder,这次我将采用原始的写法,不用BaseQuickAdapter,那个太方便搞得我原始的都不会写了

    新建CityAdapter类,让这个适配器继承自RecyclerView.Adapter,并将泛型指定为RecyclerView.ViewHolder,其代表我们在CityAdapter中定义的内部类

    public class CityAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{

    ......
    //字母头
    public static class HeadViewHolder extends RecyclerView.ViewHolder {
    private TextView tvHead;
    public HeadViewHolder(View itemView) {
    super(itemView);
    tvHead = itemView.findViewById(R.id.tv_item_head);
    }
    }

    //城市
    public static class CityViewHolder extends RecyclerView.ViewHolder {

    private TextView tvCity;
    public CityViewHolder(View itemView) {
    super(itemView);
    tvCity = itemView.findViewById(R.id.tv_item_city);
    }
    }
    }

    重写onCreateViewHolder()onBindViewHolder()getItemCount()方法,因为涉及多布局,还需重写getItemViewType()方法来区分是哪种布局

    完整代码如下

    public class CityAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    //数据项
    private List dataList;
    //点击事件监听接口
    private OnRecyclerViewClickListener onRecyclerViewClickListener;

    public void setOnItemClickListener(OnRecyclerViewClickListener onItemClickListener) {
    this.onRecyclerViewClickListener = onItemClickListener;
    }
    public CityAdapter(List dataList) {
    this.dataList = dataList;
    }
    //创建ViewHolder实例
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {

    if (viewType == 0) { //Head头字母名称
    View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item_head, viewGroup,false);
    RecyclerView.ViewHolder headViewHolder = new HeadViewHolder(view);
    return headViewHolder;
    } else { //城市名
    View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item_city, viewGroup,false);
    RecyclerView.ViewHolder cityViewHolder = new CityViewHolder(view);
    view.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    if (onRecyclerViewClickListener != null) {
    onRecyclerViewClickListener.onItemClickListener(v);
    }
    }
    });
    return cityViewHolder;
    }
    }
    //对子项数据进行赋值
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {

    int itemType = dataList.get(position).getType();
    if (itemType == 0) {
    HeadViewHolder headViewHolder = (HeadViewHolder) viewHolder;
    headViewHolder.tvHead.setText(dataList.get(position).getItemName());
    } else {
    CityViewHolder cityViewHolder = (CityViewHolder) viewHolder;
    cityViewHolder.tvCity.setText(dataList.get(position).getItemName());
    }
    }
    //数据项个数
    @Override
    public int getItemCount() {
    return dataList.size();
    }
    //区分布局类型
    @Override
    public int getItemViewType(int position) {
    int type = dataList.get(position).getType();
    return type;
    }
    //字母头
    public static class HeadViewHolder extends RecyclerView.ViewHolder {
    private TextView tvHead;
    public HeadViewHolder(View itemView) {
    super(itemView);
    tvHead = itemView.findViewById(R.id.tv_item_head);
    }
    }
    //城市
    public static class CityViewHolder extends RecyclerView.ViewHolder {
    private TextView tvCity;
    public CityViewHolder(View itemView) {
    super(itemView);
    tvCity = itemView.findViewById(R.id.tv_item_city);
    }
    }
    }

    两种item布局都是只放了一个TextView控件

    这里有两处自己碰到和当时使用ListView不同的地方:

    1、RecyclerView没有setOnItemClickListener(),需要自己定义接口来实现 2、自己平时加载布局都直接是View view = LayoutInflater.from(context).inflate(R.layout.item_head, null);,也没发现什么问题,但此次就出现了Item子布局无法横向铺满父布局。 解决办法:将改为以下方式加载布局

    View view = LayoutInflater.from(context).inflate(R.layout.item_head, viewGroup,false);

    (如果遇到不能铺满状况也可能是RecyclerView没有明确宽高而是用权重代替的原因)

    建立的监听器

    public interface OnRecyclerViewClickListener {
    void onItemClickListener(View view);
    }


    4. 绘制侧边字母栏

    这里的自定义很简单,无非是定义画笔,然后在画布上通过drawText()方法来绘制Text即可。

    4.1 首先定义类SideBar继承自View,重写构造方法,并在三个方法内调用自定义的init();方法来初始化画笔

    public class SideBar extends View {
    //画笔
    private Paint paint;

    public SideBar(Context context) {
    super(context);
    init();
    }
    public SideBar(Context context, AttributeSet attrs) {
    super(context, attrs);
    init();
    }
    public SideBar(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init();
    }
    //初始化画笔工具
    private void init() {
    paint = new Paint();
    paint.setAntiAlias(true);//抗锯齿
    }
    }

    4.2 在onDraw()方法里绘制字母

    public static String[] characters = new String[]{"❤", "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"};
    private int position = -1; //当前选中的位置
    private int defaultTextColor = Color.parseColor("#D2D2D2"); //默认拼音文字的颜色
    private int selectedTextColor = Color.parseColor("#2DB7E1"); //选中后的拼音文字的颜色

    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    int height = getHeight(); //当前控件高度
    int width = getWidth(); //当前控件宽度
    int singleHeight = height / characters.length; //每个字母占的长度

    for (int i = 0; i < characters.length; i++) {
    if (i == position) { //当前选中
    paint.setColor(selectedTextColor); //设置选中时的画笔颜色
    } else { //未选中
    paint.setColor(defaultTextColor); //设置未选中时的画笔颜色
    }
    paint.setTextSize(textSize); //设置字体大小

    //设置绘制的位置
    float xPos = width / 2 - paint.measureText(characters[i]) / 2;
    float yPos = singleHeight * i + singleHeight;

    canvas.drawText(characters[i], xPos, yPos, paint); //绘制文本
    }
    }

    通过以上两步,右侧边栏就算绘制完成了,但这只是静态的,如果要实现侧边栏滑动的时候,我们还需要监听其触摸事件

    4.3 定义触摸回调接口和设置监听器的方法

    //设置触摸位置改变的监听器的方法
    public void setOnTouchingLetterChangedListener(OnTouchingLetterChangedListener onTouchingLetterChangedListener) {
    this.onTouchingLetterChangedListener = onTouchingLetterChangedListener;
    }

    //触摸位置更改的接口
    public interface OnTouchingLetterChangedListener {
    void onTouchingLetterChanged(int position);
    }

    4.4 触摸事件

    @Override
    public boolean onTouchEvent(MotionEvent event) {

    int action = event.getAction();
    float y = event.getY();
    position = (int) (y / (getHeight() / characters.length)); //获取触摸的位置

    if (position >= 0 && position < characters.length) {
    //触摸位置变化的回调
    onTouchingLetterChangedListener.onTouchingLetterChanged(position);

    switch (action) {
    case MotionEvent.ACTION_UP:
    setBackgroundColor(Color.TRANSPARENT);//手指起来后的背景变化
    position = -1;
    invalidate();//重新绘制控件
    if (text_dialog != null) {
    text_dialog.setVisibility(View.INVISIBLE);
    }
    break;
    default://手指按下
    setBackgroundColor(touchedBgColor);
    invalidate();
    text_dialog.setText(characters[position]);//字母框的弹出
    break;
    }
    } else {
    setBackgroundColor(Color.TRANSPARENT);
    if (text_dialog != null) {
    text_dialog.setVisibility(View.INVISIBLE);
    }
    }
    return true; //一定要返回true,表示拦截了触摸事件
    }

    具体的解释如代码所示,当手指起来时,position为-1,当手指按下,更改背景并弹出字母框(这里的字母框其实就是一个TextView,通过显示隐藏来表示其弹出)

    5. Activity中使用

    itemList数据填充那些就不写了,在前面整理数据那部分

    //所有的item子项,可能是城市、可能是字母
    private List itemList;
    //目标项是否在最后一个可见项之后
    private boolean mShouldScroll;
    //记录目标项位置(要移动到的位置)
    private int mToPosition;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    //为左侧RecyclerView设立Item的点击事件
    cityAdapter.setOnItemClickListener(this);

    sideBar.setOnTouchingLetterChangedListener(new SideBar.OnTouchingLetterChangedListener() {
    @Override
    public void onTouchingLetterChanged(int position) {

    String city_label = SideBar.characters[position]; //滑动到的字母
    for (int i = 0; i < cityList.size(); i++) {
    if (itemList.get(i).getItemName().equals(city_label)) {
    moveToPosition(i); //直接滚过去
    // smoothMoveToPosition(recyclerView,i); //平滑的滚动
    tvDialog.setVisibility(View.VISIBLE);
    break;
    }
    if (i == cityList.size() - 1) {
    tvDialog.setVisibility(View.INVISIBLE);
    }
    }
    }
    });
    }

    //实战中可能会有选择完后此页面关闭,返回当前数据等操作,可在此处完成
    @Override
    public void onItemClickListener(View view) {
    int position = recyclerView.getChildAdapterPosition(view);
    Toast.makeText(view.getContext(), itemList.get(position).getItemName(), Toast.LENGTH_SHORT).show();
    }

    在使用ListView的时候,知道要移动到的位置position时,直接listView.setSelection(position)就可将当前的item移动到屏幕顶部,而RecyclerView的scrollToPosition(position)只是将item移动到屏幕内,所以需要我们通过scrollToPositionWithOffset()方法将其置顶

    private void moveToPosition(int position) {
    if (position != -1) {
    recyclerView.scrollToPosition(position);
    LinearLayoutManager mLayoutManager =
    (LinearLayoutManager) recyclerView.getLayoutManager();
    mLayoutManager.scrollToPositionWithOffset(position, 0);
    }
    }

    6. 总结

    再次说明下自己遇到的几个问题:

    1、点击问题,ListViewsetOnItemClickListener()方法,而RecyclerView没有,需要建立接口进行监听。 2、滑动问题,listViewsetSelection(position)滑动可以直接将该项滑至屏幕顶部,而recyclerView的 smoothScrollToPosition(position);只是将其移动至屏幕内,需要再次进行处理。 3、listViewisEnable() 方法可以设置字母Item不能点击,而城市名Item可以点击,recycleView的实现(直接在设立点击事件的时候,是头部就不设立点击事件就行) 4、item不充满全屏,加载布局的原因


    代码下载:AndroidSlidbar.zip

    收起阅读 »

    面试题:介绍一下 LiveData 的 postValue ?

    很多面试官喜欢会就一个问题不断深入追问。 例如一个小小的 LiveData 的 postValue,就可能会问出一连串问题: postValue 与 setValue postValue 与 setValue 一样都是用来更新 LiveData 数据...
    继续阅读 »

    很多面试官喜欢会就一个问题不断深入追问。


    例如一个小小的 LiveData 的 postValue,就可能会问出一连串问题:


    image.png


    postValue 与 setValue


    postValuesetValue 一样都是用来更新 LiveData 数据的方法:



    • setValue 只能在主线程调用,同步更新数据

    • postValue 可在后台线程调用,其内部会切换到主线程调用 setValue


    liveData.postValue("a");
    liveData.setValue("b");

    上面代码,a 在 b 之后才被更新。


    postValue 收不到通知


    postValue 使用不当,可能发生接收到数据变更的通知:



    If you called this method multiple times before a main thread executed a posted task, only the last value would be dispatched.



    如上,源码的注释中明确记载了,当连续调用 postValue 时,有可能只会收到最后一次数据更新通知。


    梳理源码可以了解其中原由:


    protected void postValue(T value) {
    boolean postTask;
    synchronized (mDataLock) {
    postTask = mPendingData == NOT_SET;
    mPendingData = value;
    }
    if (!postTask) {
    return;
    }
    ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
    }

    mPendingData 被成功赋值 value 后,post 了一个 Runnable


    mPostValueRunnable 的实现如下:


    private final Runnable mPostValueRunnable = new Runnable() {
    @SuppressWarnings("unchecked")
    @Override
    public void run() {
    Object newValue;
    synchronized (mDataLock) {
    newValue = mPendingData;
    mPendingData = NOT_SET;
    }
    setValue((T) newValue);
    }
    };


    • postValue 将数据存入 mPendingDatamPostValueRunnable 在UI线程消费mPendingData


    • 在 Runnable 中 mPendingData 值还没有被消费之前,即使连续 postValue , 也不会 post 新的 Runnable


    • mPendingData 的生产 (赋值) 和消费(赋 NOT_SET) 需要加锁



    这也就是当连续 postValue 时只会收到最后一次通知的原因。


    源码梳理过了,但是为什么要这样设计呢?


    为什么 Runnable 只 post 一次?


    mPenddingData 中有数据不断更新时,为什么 Runnable 不是每次都 post,而是等待到最后只 post 一次?


    一种理解是为了兼顾性能,UI只需显示最终状态即可,省略中间态造成的频发刷新。这或许是设计目的之一,但是一个更为合理的解释是:即使 post 多次也没有意义,所以只 post 一次即可


    我们知道,对于 setValue 来说,连续调用多次,数据会依次更新:


    如下,订阅方一次收到 a b 的通知


    liveData.setValue("a");
    liveData.setValue("b");

    通过源码可知,dispatchingValue() 中同步调用 Observer#onChanged(),依次通知订阅方:


    //setValue源码

    @MainThread
    protected void setValue(T value) {
    assertMainThread("setValue");
    mVersion++;
    mData = value;
    dispatchingValue(null);
    }

    但对于 postValue,如果当 value 变化时,我们立即post,而不进行阻塞


    protected void postValue(T value) {
    mPendingData = value;
    ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
    }

    private final Runnable mPostValueRunnable = new Runnable() {
    public void run() {
    setValue((T) mPendingData);
    }
    };

    liveData.postValue("a")
    liveData.postValue("b")

    由于线程切换的开销,连续调用 postValue,收到通知只能是b、b,无法收到a。


    因此,post 多次已无意义,一次即可。


    为什么要加读写锁?


    前面已经知道,是否 post 取决于对 mPendingData 的判断(是否为 NOT_SET)。因为要在多线程环境中访问 mPendingData ,不加读写锁无法保证其线程安全。


    protected void postValue(T value) {
    boolean postTask = mPendingData == NOT_SET; // --1
    mPendingData = value; // --2
    if (!postTask) {
    return;
    }
    ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
    }

    private final Runnable mPostValueRunnable = new Runnable() {
    public void run() {
    Object newValue = mPendingData;
    mPendingData = NOT_SET; // --3
    setValue((T) newValue);
    }
    };

    如上,如果在 1 和 2 之间,执行了 3,则 2 中设置的值将无法得到更新


    使用RxJava替换LiveData


    如何避免在多线程环境下不漏掉任何一个通知? 比较好的思路是借助 RxJava 这样的流式框架,任何数据更新都以数据流的形式发射出来,这样就不会丢失了。


    fun <T> Observable<T>.toLiveData(): LiveData<T> = RxLiveData(this)

    class RxLiveData<T>(
    private val observable: Observable<T>
    ) : LiveData<T>() {
    private var disposable: Disposable? = null

    override fun onActive() {
    disposable = observable
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe({
    setValue(it)
    }, {
    setValue(null)
    })
    }

    override fun onInactive() {
    disposable?.dispose()
    }
    }

    最后


    想要保证事件在线程切换过程中的顺序性和完整性,需要使用RxJava这样的流式框架。


    有时候面试官会使用追问的形式来挖掘候选人的技术深度,所以大家在准备面试时要多问自己几个问什么,知其然并知其所以然。


    当然,我也不赞同这种刨根问底式的拷问方式,尤其是揪着一些没有实用价值的细枝末节不放。所以本文也是提醒广大面试官,挖掘深度的同时要注意分寸,不能以将候选人难倒为目标来问问题。



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

    基于FakerAndroid的一次il2cpp游戏逆向精修实录!!!零汇编零二进制纯编码实现

    ~~~格式优化整理~~~1、下载FakerAndroid工具包 下载地址:https://github.com/Efaker/FakerAndroid/releases 2、cmd切换到FakerAndroid.jar平级目录  [工具包和...
    继续阅读 »

    ~~~格式优化整理~~~
    1、下载FakerAndroid工具包
    下载地址:https://github.com/Efaker/FakerAndroid/releases 
    2、cmd切换到FakerAndroid.jar平级目录 
    [工具包和要操作的Apk]

    [工具包目录]

    3、执行 java -jar FakerAndroid.jar fk <apkpath>生成AndroidStudio工程
    [执行命令]

    [等待命令执行完成]

    4、查看Apk平级目录下面生成的AndroidStudio工程
    [查看原安装包目录]

    5、AndroidStudio直接打开生成的Android工程
    [生成的Android项目工程目录结构]

    6、等待加载完成直接运行项目(确认项目加载完成,部分Res或Manifest文件有问题的话需要手动修复一下,实测大部分的未做res混淆的Apk都是没有问题的)
    [直接Run运行项目]

    7、Java类调用之继承(意在演示Java层原有Java类调用)
    [父类继承]

    8、Java类调用之Api调用(意在演示Java层原有Java Api调用)
    [父类Api调用]

    9、Manifest入口Activity替换
    [AndroidManifest入口Activity替换]

    10、Java类替换(意在演示对原有Java类的直接替换)
    [类替换之原类]

    [类替换之自己编写的替换类]

    11、定义Jni方法进行Hoook操作和il2cpp脚手架的调用
    [Jni方法定义]

    [HookApi和Il2cpp脚手架的使用]

    12、对原il2cpp脚手架定义过的方法进行Hook替换
    [Il2cpp脚手架中的UI回调函数替换以及Il2cpp脚手架中的Api调用]

    [JniHook Btn]

    13、最后上一下效果图,忘记说了,文章中所有图片的宽度都使用了1024px
    [效果图]

    收起阅读 »

    前端智能化看"低代码/无代码"

    概念 什么是低代码/无代码开发?业界对于低代码/无代码开发是否存在其他不同的理解? 行业里流行观点,低代码是更加易用的搭建系统,无代码是图形化和可视化编程。这种观点把低代码和无代码开发分别置于 UI 和逻辑两个环节,以工具属性定义搭建和可视化编程要解决的问题。...
    继续阅读 »

    概念


    什么是低代码/无代码开发?业界对于低代码/无代码开发是否存在其他不同的理解?


    行业里流行观点,低代码是更加易用的搭建系统,无代码是图形化和可视化编程。这种观点把低代码和无代码开发分别置于 UI 和逻辑两个环节,以工具属性定义搭建和可视化编程要解决的问题。另一种观点则是把低代码/无代码看作一个方法的两个阶段,就像对自动驾驶的 L0 ~ L5 共 6 个不同阶段一样,把我之前在:《人机协同的编程方式》 一文提出的人机协同编程的概念,划分为低代码/无代码两个阶段。较之第一种我更加认同第二种观点,不仅因为是我提出的,更因为第二种观点是以软件工程的统一视角定义、分析和解决问题,而第一种观点只是局部和过程的优化而非颠覆性创新。


    如马云先生在香港对年轻人传授创业经验时讲到的,蒸汽机和电力解放了人类的体力,人工智能和机器学习解放了人类的脑力。马云先生在评价蒸汽机和电力带来的失业问题时讲到,人类在科技进步下从繁重的体力劳动中解放出来,逐步向脑力劳动过渡,这是人类社会的进步。今天“人机协同的编程方式”把软件工程从拼装 UI 和编写业务逻辑里解放出来,逐步向业务能力、基础能力、底层能力等高技术含量工作过渡。更多内容参考:《前端智能化:思维转变之路》


    低代码开发和无代码开发之间的区别是什么?


    接着上述所答,既然低代码和无代码属于“人机协同编程”的两个阶段,低代码就是阶段一、无代码则是阶段二,分别对应“人机协作”和“人机协同”。协作和协同最大的区别就是:心有灵犀。不论低代码还是无代码,均有服务的对象:用户。不论用户是程序员还是非编程人员,均有统一目标:生成代码。不论源码开发、低代码还是无代码,都是在用不同的方式描述程序,有代码、图形、DSL……等。“人机协作”的阶段,这些描述有各种限制、约束,应用的业务场景亦狭窄。“人机协同”的阶段,则限制、约束减少,应用的业务场景亦宽广。“心有灵犀”就是指:通过 AI 对描述进行学习和理解,从而减少限制和约束,适应更多业务场景。因此,传统低代码/无代码和“人机协同编程”生成代码相比,最大的不同就是有心和无心,机器有心而平台无心。


    背景


    低代码/无代码开发与软件工程领域的一些经典思想、方法和技术,例如软件复用与构件组装、软件产品线、DSL(领域特定语言)、可视化快速开发工具、可定制工作流,以及此前业界流行的中台等概念,之间是什么关系?


    从库、框架、脚手架开始,软件工程就踏上了追求效率的道路。在这个道路之上,低代码、无代码的开发方式算是宏愿。复用、组件化和模块化、DSL、可视化、流程编排……都是在达成宏愿过程中的尝试,要么在不同环节、要么以不同方式,但都还在软件工程领域内思考。中台概念更多是在业务视角下提出的,软件工程和技术领域内类似的概念更多是叫:平台。不论中台还是平台,就不仅是在过程中的尝试,而是整体和系统的创新尝试。我提出前端智能化的“人机协同编程”应该同属于软件工程和技术领域,在类似中台的业务领域我提出“需求暨生产”的全新业务研发模式,则属于业务领域。这些概念之间无非:左右、上下、新旧关系而已。


    此外,低代码/无代码开发与DevOps、云计算与云原生架构之间又是什么样的关系?


    DevOps、云计算……都属于基础技术,基础技术的变化势必带来上层应用层技术变化。没有云计算的容器化、弹性缩扩容,做分布式系统是很困难的,尤其在 CI/CD、部署、运维、监控、调优……等环节更甚,什么南北分布、异地多活、平行扩展、高可用……都需要去关注。但是,云计算和DevOps等基础技术的发展,内化并自动化解决了上述问题,大大降低了关注和使用成本,这就是心有灵犀,在这样的基础技术之上构建应用层技术,限制少、约束小还能适应各种复杂场景。


    思想方法


    支撑低代码/无代码开发的核心技术是什么?


    我认为低代码/无代码开发的核心技术,过去是“复用”,今天是 AI 驱动的“人机协同编程”。过去的低代码/无代码开发多围绕着提升研发效能入手,今天 AI 驱动的“人机协同编程”则是围绕着提升交付效率入手。因此,低代码/无代码开发以“人机协同编程”为主要实现手段的话,AI 是其核心技术。


    低代码/无代码开发的火热是软件开发技术上的重要变革和突破,还是经典软件工程思想、方法和技术随着技术和业务积累的不断发展而焕发出的新生机?


    计算机最初只在少数人掌握,如今,几乎人人手持一台微型计算机:智慧手机。当初为程序员和所谓“技术人员”的专利,而今,几乎人人都会操作和使用计算机。然而,人们对计算机的操作是间接的,需要有专业的人士和企业提前编写软件,人们通过软件使用计算机的各种功能。随着计算机算力和功能的不断发展,随着社会的数字化和信息化,今天的人们越来越难以被提前定制好的软件所满足。低代码/无代码开发则赋予人们创造软件的能力,进而帮助人们低成本、即时、高效的直接生产符合自己需求的软件,进而操作众多复杂的电子设备和数字世界建立联结。我认为,这是不可逆的趋势,也是低代码/无代码开发的大方向。


    现状进展


    低代码/无代码开发已经发展到什么程度?


    image.png


    imgcook



    • 2w 多用户、6w 多模块、 0 前端参与研发的双十一等大促营销活动、70% 阿里前端在使用

    • 79.26% 无人工参与的线上代码可用率、90.9% 的还原度、Icon 识别准确率 83%、组件识别 85%、布局还原度 92.1%、布局人工修改概率 75%

    • 研发效率提升 68%


    uicook


    -营销活动和大促场景 ui 智能生成比例超过 90% -日常频道导购业务 ui 智能生成覆盖核心业务



    • 纯 ui 智能化和个性化带来的业务价值提升超过 8%


    bizcook


    初步完成基于 NLP 的需求标注和理解系统 初步完成基于 NLP 的服务注册和理解系统 初步完成基于 NLP 的胶水层业务逻辑代码生成能力


    reviewcook



    • 针对资损防控自动化扫描、CV 和 AI 自动化识别资损风险和舆情问题

    • 和测试同学共建的 UI 自动化测试、数据渲染和 Mock 驱动的业务自动化验证

    • 和工程团队共建的 AI Codereview 基于对代码的分析和理解,结合线上 Runtime 的识别和分析,自动化发现问题、定位问题,提升 Codereview 的效率和质量


    datacook



    • 社区化运营开源项目,合并 Denfo.js 同其作者共同设立 Datacook 项目,全链路、端到端解决 AI 领域数据采集、存储、处理问题,尤其在海量数据、数据集组织、数据质量评估等深度学习和机器学习领域的能力比肩 HDF5、Pandas……等 Python 专业 LIbrary

    • Google Tensorflow.js 团队合作开发维护 TFData library ,作为 Datacook 的核心技术和基础,共同构建数据集生态和数据集易用性


    pipcook



    • 开源了 github.com/alibaba/pip… 纯前端机器学习框架

    • 利用 Boa 打通 Python 技术生态,原生支持 import Python 流行的包和库,原生支持 Python 的数据类型和数据结构,方便跨语言共享数据和调用 API

    • 利用 Pipcook Cloud 打通流行的云计算平台,帮助前端智能化实现 CDML,形成数据和算法工程闭环,帮助开发者打造工业级可用的服务和在线、离线算法能力


    有哪些成熟的低代码/无代码开发平台?


    image.png image.png image.png


    低代码/无代码开发能够在多大程度上改变当前的软件开发方式?


    随着计算机算力和功能的不断发展,随着社会的数字化和信息化,今天的人们越来越难以被提前定制好的软件所满足。低代码/无代码开发则赋予人们创造软件的能力,进而帮助人们低成本、即时、高效的直接生产符合自己需求的软件,进而操作众多复杂的电子设备和数字世界建立联结。我认为,这是不可逆的趋势,也是低代码/无代码开发的大方向。最终,软件开发势必从专业程序员手里转向普罗大众,成为今天操作计算机一样的基本生存技能之一。因此,软件开发方式将带来本质变化,从完整的交付转向局部交付、从业务整体交付转向业务能力交付……


    展望未来


    低代码/无代码开发未来发展的方向是什么?


    要我说,低代码/无代码开发未来发展的方向一定是:AI 驱动的“人机协同编程”,将完整开发一个软件变成提供局部的软件功能,类似 Apple 的“捷径”一样,由用户决定这些局部软件功能如何组装成适合用户的软件并交付最终用户。AI 驱动提供两个方面的价值:


    降低开发成本


    以往开发软件的时候,要有 PRD、交互稿、设计稿、设计文档……等一系列需求规格说明,然后,根据这些需求规格利用技术和工程手段进行实现。然而,低代码/无代码开发交付的是局部功能和半成品,会被无法枚举的目的和环境所使用,既然无法枚举,就不能用 Swith……Case 的方式编写代码,否则会累死。


    AI 的特点就是基于特征和环境进行预测,预测的基础是对模式和本质的理解。就像 AI 识别一只猫,不管这个猫在什么环境、什么光照条件下,也不管这只猫是什么品种,AI 都能够以超过人类的准确度识别。试想,作为一个程序员用程序判断一只猫的开发成本何其高?


    降低使用成本


    今天的搭建体系,本质上是把编程过程用搭建的思想重构了一遍,工作的内容并没有发生变化,成本从程序员转嫁到运营、产品、设计师的身上。这还是其次,今天的搭建平台都是技术视角出发,充斥着运营、产品、设计等非技术人员一脸懵逼的概念,花在答疑解惑和教他们如何在页面上定制一个搜索框的时间,比自己和他们沟通后源码实现的时间还要长,而且经常在撸代码的时候被打断……


    基于 AI 的“人机协同编程”不需要透出任何技术概念,运营、产品、设计……等非技术人员也不改变其工作习惯,都用自己熟悉的工具和自己熟悉的概念描述自己的需求,AI 负责对这些需求进行识别和理解,再转换成编程和技术工程领域的概念,进而生成代码并交付,从而大幅度降低使用成本。


    举个例子:如果你英文写作能力不好,你拿着朗道词典一边翻译一边拼凑单词写出来的英文文章质量高呢?还是用中文把文章写好,再使用 Google 翻译整篇转换成英文的文章质量高?你自己试试就知道了。究其原因,你在自己熟悉的语言和概念领域内,才能够把自己的意思表达清楚。


    围绕低代码/无代码开发存在哪些技术难题需要学术界和工业界共同探索?


    最初在 D2 上提出并分享“前端智能化”这个概念的时候,我就提出:识别、理解、表达 这个核心过程。我始终认为,达成 AI 驱动的“人机协同编程”关键路径就是:识别、理解、表达。因此,围绕 AI 识别、 AI 理解、 AI 表达我们和国内外知名大学展开了广泛的合作。


    识别


    需求的识别:通过 NLP 、知识图谱、图神经网络、结构化机器学习……等 AI 技术,识别用户需求、产品需求、设计需求、运营需求、营销需求、研发需求、工程需求……等,识别出其中的概念和概念之间的关系


    设计稿的识别:通过 CV、GAN、对象识别、语义分割……等 AI 技术,识别设计稿中的元素、元素之间的关系、设计语言、设计系统、设计意图


    UI 的识别:通过用户用脚投票的结果进行回归,后验的分析识别出 UI 对用户行为的影响程度、影响效果、影响频率、影响时间……等,并识别出 UI 的可变性和这些用户行为影响之间的关系


    计算机程序的识别:通过对代码、AST ……等 Raw Data 分析,借助 NLP 技术识别计算机程序中,语言的表达能力、语言的结构、语言中的逻辑、语言和外部系统通过 API 的交互等


    日志和数据的识别:通过对日志和数据进行 NLP、回归、统计分析等方式,识别出程序的可用性、性能、易用性等指标情况,并识别出影响这些指标的日志和数据出自哪里,找出其间的关系


    理解


    横向跨领域的理解:对识别出的概念进行降维,从而在底层更抽象的维度上找出不同领域之间概念的映射关系,从而实现用不同领域的概念进行类比,进而在某领域内理解其它领域的概念


    纵向跨层次的理解:利用机器学习和深度学习的 AI 算法能力,放宽不同层次间概念的组成关系,对低层次概念实现跨层次的理解,进而形成更加丰富的技术、业务能力供给和使用机会


    常识、通识的理解:以常识、通识构建的知识图谱为基础,将 AI 所面对的开放性问题领域化,将领域内的常识和通识当做理解的基础,不是臆测和猜想,而是实实在在构建在理论基础上的理解


    表达


    个性化:借助大数据和算法实现用户和软件功能间的匹配,利用 AI 的生成能力降低千人前面的研发成本,从而真正实现个性化的软件服务能力,把软件即服务推向极致


    共情:利用端智能在用户侧部署算法模型,既可以解决用户隐私保护的问题,又可以对用户不断变化的情绪、诉求、场景及时学习并及时做出响应,从而让软件从程序功能的角度急用户之所急、想用户之所想,与用户共情、让用户共鸣。举个例子:我用 iPhone 在进入地铁站的时候,因为现在要检查健康码,每次进入地铁站 iOS 都会给我推荐支付宝快捷方式,我不用自己去寻找支付宝打开展示健康码,这就让我感觉 iOS 很智能、很贴心,这就是共情。


    后记


    从提出前端智能化这个概念到现在已历三年,最初,保持着“让前端跟上 AI 发展的浪潮”的初心上路,到“解决一线研发问题”发布 imgcook.com ,再到“给前端靠谱的机器学习框架”开源github.com/alibaba/pip…


    这一路走来,几乎日日夜不能寐。真正想从本质上颠覆现在的编程模式和研发模式谈何容易?这个过程中,我们从一群纯前端变成前端和 AI 的跨界程序员,开发方式从写代码到机器生成,周围的人从作壁上观到积极参与,正所谓:念念不忘,必有回响。低代码/无代码开发方兴未艾,广大技术、科研人员在这个方向上厉兵秣马,没有哪个方法是 Silverbullet ,也没有哪个理论是绝对正确的,只要找到你心中所爱,坚持研究和实践,终会让所有人都能够自定义软件来操作日益复杂和强大的硬件设备,终能让所有人更加便捷、直接、有效的接入数字世界,终于在本质上将软件开发和软件工程领域重新定义!共勉!



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

    收起阅读 »

    iOS 开发的应用内调试和探索工具-FLEX

    FLEX (Flipboard Explorer) 是一套用于 iOS 开发的应用内调试和探索工具。出现时,FLEX 会显示一个位于应用程序上方窗口中的工具栏。从此工具栏上,您可以查看和修改正在运行的应用程序中的几乎所有状态。给自己调试超能力检查和修改层次结构...
    继续阅读 »

    FLEX (Flipboard Explorer) 是一套用于 iOS 开发的应用内调试和探索工具。出现时,FLEX 会显示一个位于应用程序上方窗口中的工具栏。从此工具栏上,您可以查看和修改正在运行的应用程序中的几乎所有状态。

    给自己调试超能力

    • 检查和修改层次结构中的视图。
    • 查看任何对象的属性和变量。
    • 动态修改许多属性和变量。
    • 动态调用实例和类方法。
    • 通过时间、标头和完整响应观察详细的网络请求历史记录。
    • 添加您自己的模拟器键盘快捷键。
    • 查看系统日志消息(例如来自NSLog)。
    • 通过扫描堆访问任何活动对象。
    • 查看应用程序沙箱中的文件系统。
    • 浏览文件系统中的 SQLite/Realm 数据库。
    • 使用 control、shift 和 command 键在模拟器中触发 3D 触摸。
    • 探索您的应用程序和链接系统框架(公共和私有)中的所有类。
    • 快速访问有用的对象,例如[UIApplication sharedApplication]应用程序委托、关键窗口上的根视图控制器等。
    • 动态查看和修改NSUserDefaults值。

    与许多其他调试工具不同,FLEX 完全在您的应用程序内部运行,因此您无需连接到 LLDB/Xcode 或其他远程调试服务器。它在模拟器和物理设备上运行良好。用法

    在 iOS 模拟器中,您可以使用键盘快捷键来激活 FLEX。f将切换 FLEX 工具栏。敲击?快捷键的完整列表。您还可以以编程方式显示 FLEX:

    // Objective-C
    [[FLEXManager sharedManager] showExplorer];

    // Swift
    FLEXManager.shared.showExplorer()

    更完整的版本:

    #if DEBUG
    #import "FLEXManager.h"
    #endif

    ...

    - (void)handleSixFingerQuadrupleTap:(UITapGestureRecognizer *)tapRecognizer
    {
    #if DEBUG
    if (tapRecognizer.state == UIGestureRecognizerStateRecognized) {
    // This could also live in a handler for a keyboard shortcut, debug menu item, etc.
    [[FLEXManager sharedManager] showExplorer];
    }
    #endif
    }


    功能示例

    修改视图

    选择视图后,您可以点击工具栏下方的信息栏以显示有关该视图的更多详细信息。从那里,您可以修改属性和调用方法。



    网络历史

    启用后,网络调试允许您查看使用 NSURLConnection 或 NSURLSession 发出的所有请求。设置允许您调整缓存的响应主体类型和响应缓存的最大大小限制。您可以选择在应用启动时自动启用网络调试。此设置在启动时保持不变。



    堆上的所有对象

    FLEX 查询 malloc 以获取所有实时分配的内存块并搜索看起来像对象的内存块。你可以从这里看到一切。

    堆/活动对象资源管理器


    探索地址

    如果您获得任意地址,您可以尝试探索该地址处的对象,如果 FLEX 可以验证该地址指向有效对象,则会打开它。如果 FLEX 不确定,它会警告您并拒绝取消对指针的引用。但是,如果您更了解,则可以通过选择“不安全探索”来选择探索它

    地址浏览器


    模拟器键盘快捷键

    默认键盘快捷键允许您激活 FLEX 工具、使用箭头键滚动以及使用转义键关闭模式。您还可以通过添加自定义键盘快捷键-[FLEXManager registerSimulatorShortcutWithKey:modifiers:action:description]

    模拟器键盘快捷键


    安装

    CocoaPods

    pod 'FLEX', :configurations => ['Debug']

    Carthage

    1. 不要添加FLEX.framework到目标的嵌入式二进制文件中,否则它会包含在所有构建中(因此也包含在发布版本中)。

    2. 相反,添加$(PROJECT_DIR)/Carthage/Build/iOS到您的目标框架搜索路径(如果您已经在 Carthage 中包含了其他框架,则此设置可能已经存在)。这使得从源文件导入 FLEX 框架成为可能。如果为所有配置添加此设置也无害,但至少应为调试添加此设置。

    3. 向您的目标添加一个运行脚本阶段Link Binary with Libraries例如,在现有阶段之后插入它),并且它只会嵌入FLEX.framework到调试版本中:

    if [ "$CONFIGURATION" == "Debug" ]; then
    /usr/local/bin/carthage copy-frameworks
    fi
    最后,添加
    $(SRCROOT)/Carthage/Build/iOS/FLEX.framework为这个脚本阶段的输入文件。

    手动添加到项目的 FLEX 文件

    在 Xcode 中,导航到Build Settings > Build Options > Excluded Source File Names对于您的Release配置,将其设置为FLEX*这样以排除具有FLEX前缀的所有文件


    常见问题及demo下载:https://github.com/FLEXTool/FLEX







    收起阅读 »

    Android 抛弃旧有逆向方式,如何快速逆向:FakerAndroid

    FakerAndroidA tool translate apk file to common android project and support so hook and include il2cpp c++ scaffolding when apk is...
    继续阅读 »

    FakerAndroid

    A tool translate apk file to common android project and support so hook and include il2cpp c++ scaffolding when apk is a il2cpp game apk

    简介

    • 优雅地在一个Apk上写代码
    • 直接将Apk文件转换为可以进行二次开发的Android项目的工具,支持so hook,对于il2cpp的游戏apk直接生成il2cpp c++脚手架
    • 将痛苦的逆向环境,转化为舒服的开发环境,告别汇编,告别二进制,还有啥好说的~~

    特点

    • 提供Java层代码覆盖及继承替换的脚手架,实现java与smali混编
    • 提供so函数Hook Api
    • 对于il2cpp的游戏apk直接生成il2cpp c++脚手架
    • Java层标准的对原有Java api的AndroidStudio编码提示
    • Smali文件修改后运行或打包时自动回编译(AndroidStudio project 文件树模式下可以直接找到smali文件,支持对smali修改,最小文件数增量编译)
    • 对于il2cpp的游戏apk,标准的Jni对原有il2cpp脚本的编码提示
    • 无限的可能性和扩展性,能干啥你说了算~
    • Dex折叠,对敏感已经存在或后续接入的代码进行隐藏规避静态分析

    运行环境

    使用方式

    • 下载FakerAndroid.jar(2020/11/15/16:53:00)
    • cmd命令行 cd <FakerAndroid.jar平级目录>
    • cmd命令行 java -jar FakerAndroid.jar fk <apkpath>(项目生成路径与apk文件平级) 或 java -jar FakerAndroid.jar fk <apkpath> -o <outdir>
    • 例:java -jar FakerAndroid.jar fk D:\apk\test.apk或 java -jar FakerAndroid.jar fk D:\apk\test.apk -o D:\test

    或者使用方式

    • 下载FakerAndroid-AS.zip(2020/11/15/16:53:00)
    • AS->File-Settings->Plugin->SettingIcon->InstallPlugin Plugin From Disk(选择FakerAndroid-AS.zip-安装-启用)->重启AndroidStudio
    • AS->File->FakerAndroid->选择目标Apk文件

    生成的Android项目二次开发教程(图文教程)

    1、打开项目
    • Android studio直接打开工具生成的Android项目
    • 保持跟目录build.gradle中依赖固定,请勿配置AndroidGradlePlugin,且项目配置NDk版本为21
    • 存在已知缺陷,res下的部分资源文件编译不过,需要手动修复一下,部分Manifest标签无法编译需要手动修复
      (关于Res混淆手动实验了几个,如果遇到了这个问题,可以手动尝试,只要保证res/public.xml中的name对应的资源文件可以正常链路下去然后修复到可编译的程度,程序运行时一般是没有res问题,太完美的解决方案尚未完成)
    2、调试运行项目
    • 连接测试机机
    • Run项目
    3、进阶
    • 类调用
      借助javaScaffoding 在主模块(app/src/main/java)编写java代码对smali代码进行调用
    • 类替换
      在主模块(app/src/main/java)直接编写Java类,类名与要替换的类的smali文件路径对应
    • Smali 增量编译
      你可以使用传统的smali修改方式对smali代码进行修改,且编译方式为最小文件数增量编译,smali文件修改后javascaffoding会同步,比如遇到final或private的java元素无法掉用时可以先修改smali(执行一次编译后javaScaffoding会同步)
    • So Hook
      借助FakeCpp 使用jni对so函数进行hook替换
    • il2cpp unity游戏脚本二次开发
      借助il2cpp Scaffolding 和FakeCpp,使用jni对原il2cpp游戏脚本进行Hook调用
    • Dex折叠
      build.gradle 配置sensitiveOptions用于隐藏敏感的dex代码,以规避静态分析,(Dex缓存原因在app版本号不变的情况使用第一次缓存,配置项调试请卸载后运行)
    4、正在路上

    resources.arsc decode 兼容,目前混淆某些大型 apk Res decoder有问题
    各种不理想情况兼容

    5、兼容性

    1、目前某些大型的apk资做过资源文件混淆的会有问题!
    2、Google play 90% 游戏apk可以一马平川
    3、加固Apk需要先脱壳后才能,暴漏java api
    4、有自校验的Apk,须项目运行起来后自行检查破解
    5、Manifest莫名奇妙的问题,可以先尝试注释掉异常代码,逐步还原试试
    6、Java OOM issue
    7、AS打不开,试试Help->Change Memery Settings(搞大点)

    github地址:https://github.com/Efaker/FakerAndroid

    下载地址:FakerAndroid.zip


    收起阅读 »

    使用 iOS OpenGL ES 实现长腿功能

    本文介绍了如何使用 OpenGL ES 来实现长腿功能。学习这个例子可以加深我们对纹理渲染流程的理解。另外,还会着重介绍一下「渲染到纹理」这个新知识点。警告: 本文属于进阶教程,阅读前请确保已经熟悉 OpenGL ES 纹理渲染的相关概念,否则强行阅读可能导致...
    继续阅读 »


    本文介绍了如何使用 OpenGL ES 来实现长腿功能。学习这个例子可以加深我们对纹理渲染流程的理解。另外,还会着重介绍一下「渲染到纹理」这个新知识点。

    警告: 本文属于进阶教程,阅读前请确保已经熟悉 OpenGL ES 纹理渲染的相关概念,否则强行阅读可能导致走火入魔。传送门

    注: 下文中的 OpenGL ES 均指代 OpenGL ES 2.0。

    一、效果展示

    首先来看一下最终的效果,这个功能简单来说,就是实现了图片的局部拉伸,从逻辑上来说并不复杂。


    二、思路

    1、怎么实现拉伸

    我们来回忆一下,我们要渲染一张图片,需要将图片拆分成两个三角形,如下所示:


    如果我们想对图片进行拉伸,很简单,只需要修改一下 4 个顶点坐标的 Y 值即可。


    那么,如果我们只想对图片中间的部分进行拉伸,应该怎么做呢?

    其实答案也很容易想到,我们只需要修改一下图片的拆分方式。如下所示,我们把图片拆分成了 6 个三角形,也可以说是 3 个小矩形。这样,我们只需要对中间的小矩形做拉伸处理就可以了。


    2、怎么实现重复调整

    我们观察上面的动态效果图,可以看到第二次的压缩操作,是基于第一次的拉伸操作的结果来进行的。因此,在每一步我们都需要拿到上一步的结果,作为原始图,进行再次调整。

    这里的「原始图」就是一个纹理。换句话说,我们需要将每一次的调整结果,都重新生成一个纹理,供下次调整的时候使用。

    这一步是本文的重点,我们会通过「渲染到纹理」的方式来实现,具体的步骤我们在后面会详细介绍。

    三、为什么要使用 OpenGL ES

    可能有人会说:你这个功能平平无奇,就算不懂 OpenGL ES,我用其它方式也能实现呀。

    确实,在 iOS 中,我们绘图一般是使用 CoreGraphics。假设我们使用 CoreGraphics,也按照上面的实现思路,对原图进行拆分绘制,重复调整的时候进行重新拼接,目测也是能实现相同的功能。

    但是,由于 CoreGraphics 绘图依赖于 CPU,当我们在调节拉伸区域的时候,需要不断地进行重绘,此时 CPU 的占用必然会暴涨,从而引起卡顿。而使用 OpenGL ES 则不存在这样的问题。

    四、实现拉伸逻辑

    从上面我们知道,渲染图片我们需要 8 个顶点,而拉伸逻辑的关键就是顶点坐标的计算,在拿到计算结果后再重新渲染。

    计算顶点的关键步骤如下:

    /**
    根据当前控件的尺寸和纹理的尺寸,计算初始纹理坐标

    @param size 原始纹理尺寸
    @param startY 中间区域的开始纵坐标位置 0~1
    @param endY 中间区域的结束纵坐标位置 0~1
    @param newHeight 新的中间区域的高度
    */
    - (void)calculateOriginTextureCoordWithTextureSize:(CGSize)size
    startY:(CGFloat)startY
    endY:(CGFloat)endY
    newHeight:(CGFloat)newHeight {
    CGFloat ratio = (size.height / size.width) *
    (self.bounds.size.width / self.bounds.size.height);
    CGFloat textureWidth = self.currentTextureWidth;
    CGFloat textureHeight = textureWidth * ratio;

    // 拉伸量
    CGFloat delta = (newHeight - (endY - startY)) * textureHeight;

    // 判断是否超出最大值
    if (textureHeight + delta >= 1) {
    delta = 1 - textureHeight;
    newHeight = delta / textureHeight + (endY - startY);
    }

    // 纹理的顶点
    GLKVector3 pointLT = {-textureWidth, textureHeight + delta, 0}; // 左上角
    GLKVector3 pointRT = {textureWidth, textureHeight + delta, 0}; // 右上角
    GLKVector3 pointLB = {-textureWidth, -textureHeight - delta, 0}; // 左下角
    GLKVector3 pointRB = {textureWidth, -textureHeight - delta, 0}; // 右下角

    // 中间矩形区域的顶点
    CGFloat startYCoord = MIN(-2 * textureHeight * startY + textureHeight, textureHeight);
    CGFloat endYCoord = MAX(-2 * textureHeight * endY + textureHeight, -textureHeight);
    GLKVector3 centerPointLT = {-textureWidth, startYCoord + delta, 0}; // 左上角
    GLKVector3 centerPointRT = {textureWidth, startYCoord + delta, 0}; // 右上角
    GLKVector3 centerPointLB = {-textureWidth, endYCoord - delta, 0}; // 左下角
    GLKVector3 centerPointRB = {textureWidth, endYCoord - delta, 0}; // 右下角

    // 纹理的上面两个顶点
    self.vertices[0].positionCoord = pointLT;
    self.vertices[0].textureCoord = GLKVector2Make(0, 1);
    self.vertices[1].positionCoord = pointRT;
    self.vertices[1].textureCoord = GLKVector2Make(1, 1);
    // 中间区域的4个顶点
    self.vertices[2].positionCoord = centerPointLT;
    self.vertices[2].textureCoord = GLKVector2Make(0, 1 - startY);
    self.vertices[3].positionCoord = centerPointRT;
    self.vertices[3].textureCoord = GLKVector2Make(1, 1 - startY);
    self.vertices[4].positionCoord = centerPointLB;
    self.vertices[4].textureCoord = GLKVector2Make(0, 1 - endY);
    self.vertices[5].positionCoord = centerPointRB;
    self.vertices[5].textureCoord = GLKVector2Make(1, 1 - endY);
    // 纹理的下面两个顶点
    self.vertices[6].positionCoord = pointLB;
    self.vertices[6].textureCoord = GLKVector2Make(0, 0);
    self.vertices[7].positionCoord = pointRB;
    self.vertices[7].textureCoord = GLKVector2Make(1, 0);
    }

    五、渲染到纹理

    上面提到:我们需要将每一次的调整结果,都重新生成一个纹理,供下次调整的时候使用。

    出于对结果分辨率的考虑,我们不会直接读取当前屏幕渲染结果对应的帧缓存,而是采取「渲染到纹理」的方式,重新生成一个宽度与原图一致的纹理。

    这是为什么呢?

    假设我们有一张 1000 X 1000 的图片,而屏幕上的控件大小是 100 X 100 ,则纹理渲染到屏幕后,渲染结果对应的渲染缓存的尺寸也是 100 X 100 (暂不考虑屏幕密度)。如果我们这时候直接读取屏幕的渲染结果,最多也只能读到 100 X 100 的分辨率。

    这样会导致图片的分辨率下降,所以我们会使用能保持原有分辨率的方式,即「渲染到纹理」。

    在这之前,我们都是将纹理直接渲染到屏幕上,关键步骤像这样:

    GLuint renderBuffer; // 渲染缓存
    GLuint frameBuffer; // 帧缓存

    // 绑定渲染缓存要输出的 layer
    glGenRenderbuffers(1, &renderBuffer);
    glBindRenderbuffer(GL_RENDERBUFFER, renderBuffer);
    [self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer];

    // 将渲染缓存绑定到帧缓存上
    glGenFramebuffers(1, &frameBuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER,
    GL_COLOR_ATTACHMENT0,
    GL_RENDERBUFFER,
    renderBuffer);

    我们生成了一个渲染缓存,并把这个渲染缓存挂载到帧缓存的 GL_COLOR_ATTACHMENT0 颜色缓存上,并通过 context 为当前的渲染缓存绑定了输出的 layer 。

    其实,如果我们不需要在屏幕上显示我们的渲染结果,也可以直接将数据渲染到另一个纹理上。更有趣的是,这个渲染后的结果,还可以被当成一个普通的纹理来使用。这也是我们实现重复调整功能的基础。

    具体操作如下:

    // 生成帧缓存,挂载渲染缓存
    GLuint frameBuffer;
    GLuint texture;

    glGenFramebuffers(1, &frameBuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);

    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, newTextureWidth, newTextureHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);

    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);

    通过对比我们可以发现,这里我们用 Texture 来替换 Renderbuffer ,并且同样是挂载到 GL_COLOR_ATTACHMENT0 上,不过这里就不需要另外再绑定 layer 了。

    另外,我们需要为新的纹理设置一个尺寸,这个尺寸不再受限于屏幕上控件的尺寸,这也是新纹理可以保持原有分辨率的原因。

    这时候,渲染的结果都会被保存在 texture 中,而 texture 也可以被当成普通的纹理来使用。

    六、保存结果

    当我们调整出满意的图片后,需要把它保存下来。这里分为两步,第一步仍然是上面提到的重新生成纹理,第二步就是把纹理转化为图片。

    第二步主要通过 glReadPixels 方法来实现,它可以从当前的帧缓存中读取出纹理数据。直接上代码:

    // 返回某个纹理对应的 UIImage,调用前先绑定对应的帧缓存
    - (UIImage *)imageFromTextureWithWidth:(int)width height:(int)height {
    int size = width * height * 4;
    GLubyte *buffer = malloc(size);
    glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, buffer);
    CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, buffer, size, NULL);
    int bitsPerComponent = 8;
    int bitsPerPixel = 32;
    int bytesPerRow = 4 * width;
    CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();
    CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault;
    CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;
    CGImageRef imageRef = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent);

    // 此时的 imageRef 是上下颠倒的,调用 CG 的方法重新绘制一遍,刚好翻转过来
    UIGraphicsBeginImageContext(CGSizeMake(width, height));
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    free(buffer);
    return image;
    }

    至此,我们已经拿到了 UIImage 对象,可以把它保存到相册里了。

    源码

    请到 GitHub 上查看完整代码。

    参考

    iOS 中使用 OpenGL 实现增高功能
    学习 OpenGL ES 之渲染到纹理
    获取更佳的阅读体验,请访问原文地址【Lyman's Blog】使用 iOS OpenGL ES 实现长腿功能

    链接:https://www.jianshu.com/p/433f13a2945e

    收起阅读 »

    runtime 小结

    OC被称之为动态运行时语言,最主要的原因就是因为两个特性,一个是运行时也就是runtime,一个是多态。runtimeruntime又叫运行时,是一套底层的c语言api,其为iOS内部核心之一。OC是动态运行时语言,它会将一些工作放在代码运行时去处理,而非编译...
    继续阅读 »

    OC被称之为动态运行时语言,最主要的原因就是因为两个特性,一个是运行时也就是runtime,一个是多态。

    runtime

    runtime又叫运行时,是一套底层的c语言api,其为iOS内部核心之一。OC是动态运行时语言,它会将一些工作放在代码运行时去处理,而非编译时,比如动态的遍历属性和方法,动态的添加属性和方法,动态的修改属性和方法等。

    了解runtime,首先要先了解它的核心--消息传递。

    消息传递

    消息直到运行时才会与方法实践绑定起来。
    一个实例对象调用实例方法,像这样[obj doSomething];,编译器转成消息发送objc_msgSend(obj, @selector(doSomething),,);,

    OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)

    runtime时的运行流程如下:

    1、首先通过调用对象的isa找到class;
    2、在class的method_list里面找该方法,这里如果是实例对象,则去实例对象的类的方法列表中找,如果是类对象调用类方法,则去元类的方法列表中找,具体下面解释;
    3、如果class里没找到,继续往它的superClass里找;
    4、一旦找到doSomething这个函数,就去执行它的实现IMP;

    下面介绍一下对象(object),类(class),方法(method)的结构体:

    //对象
    struct objc_object {
    Class isa OBJC_ISA_AVAILABILITY;
    };
    //类
    struct objc_class {
    Class isa OBJC_ISA_AVAILABILITY;
    #if !__OBJC2__
    Class super_class OBJC2_UNAVAILABLE;
    const char *name OBJC2_UNAVAILABLE;
    long version OBJC2_UNAVAILABLE;
    long info OBJC2_UNAVAILABLE;
    long instance_size OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
    struct objc_cache *cache OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
    #endif
    } OBJC2_UNAVAILABLE;

    //方法列表
    struct objc_method_list {
    struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
    int method_count OBJC2_UNAVAILABLE;
    #ifdef __LP64__
    int space OBJC2_UNAVAILABLE;
    #endif
    /* variable length structure */
    struct objc_method method_list[1] OBJC2_UNAVAILABLE;
    } OBJC2_UNAVAILABLE;
    //方法
    struct objc_method {
    SEL method_name OBJC2_UNAVAILABLE;
    char *method_types OBJC2_UNAVAILABLE;
    IMP method_imp OBJC2_UNAVAILABLE;
    }

    类对象(objc_class)

    OC中类是Class来表示,实际上是一个指向objc_class结构体的指针。

    //对象
    struct objc_object {
    Class isa OBJC_ISA_AVAILABILITY;
    };
    //类
    struct objc_class {
    Class isa OBJC_ISA_AVAILABILITY;
    #if !__OBJC2__
    Class super_class OBJC2_UNAVAILABLE;
    const char *name OBJC2_UNAVAILABLE;
    long version OBJC2_UNAVAILABLE;
    long info OBJC2_UNAVAILABLE;
    long instance_size OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
    struct objc_cache *cache OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
    #endif
    } OBJC2_UNAVAILABLE;
    //方法列表
    struct objc_method_list {
    struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
    int method_count OBJC2_UNAVAILABLE;
    #ifdef __LP64__
    int space OBJC2_UNAVAILABLE;
    #endif
    /* variable length structure */
    struct objc_method method_list[1] OBJC2_UNAVAILABLE;
    }

    观察一下对象的结构体和类对象的结构体,可以看到里面都有一个isa指针,对象的isa指针指向类,类的isa指针指向元类(metaClass),元类也是类,元类的isa指针最终指向根元类(rootMetaClass),根元类的isa指针指向自己,最终形成一个闭环。



    可以看到类结构体中有一个methodLists,也就解释了上文提到的成员方法记录在class method-list中,类方法记录在metaClass中。即Instance-object的信息记录在class-object中,而class-object的信息记录在meta-class中。

    结构体中有一个ivars指针指向objc_ivar_list结构体,是该类的属性列表,因为编译器编译顺序是父类,子类,分类,所以这也就是为什么分类category不能添加属性,因为类在编译的时候已经注册在runtime中了,属性列表objc_ivar_list和instance_size内存大小都已经确定了,同时runtime会调用class_setIvarLayout和class_setWeakIvarLayout来处理strong和weak引用。可以通过runtime的关联属性来给分类添加属性(原因是category结构体中有一个instanceProperties,下文会讲到)。因为编译顺序是父类,子类,分类,所以消息遍历的顺序是分类,子类,父类,先进后出。

    objc_cache结构体,是一个很有用的方法缓存,把经常调用的方法缓存下来,提高遍历效率。将方法的method_name作为key,method_imp作为value保存下来。

    Method(objc_method)

    结构体如下:

    //方法
    struct objc_method {
    SEL method_name OBJC2_UNAVAILABLE;
    char *method_types OBJC2_UNAVAILABLE;
    IMP method_imp OBJC2_UNAVAILABLE;
    }

    可以看到里面有一个SEL和IMP,这里讲一下两者的区别。

    SEL是selector的OC表示,数据结构为:typedef struct objc_selector *SEL;是个映射到方法的c字符串;不同于函数指针,函数指针直接保存了方法地址,SEL只是一个编号;也是objc_cache中的key。

    ps.这也带来了一个弊端,函数重载不适用,因为函数重载是方法名相同,参数名不同,但是SEL只记了方法名,没有参数,所以没法区分不同的method。

    ps.在不同的类中,相同的方法名,方法选择器也是相同的。

    IMP是函数指针,数据结构为typedef id (IMP)(id,SEL,**);保存了方法地址,由编译器绑定生成,最终方法执行哪段代码由IMP决定。IMP指向了方法的实现,一组id和SEL可以确定唯一的实现。

    有了SEL这个中间过程,我们可以对一个编号和方法实现做些中间操作,也就是说我们一个SEL可以指向不同的函数指针,这样就可以完成一个方法名在不同的时候执行不同的函数体。另外可以将SEL作为参数传递给不同的类执行,也就是我们某些业务只知道方法名但需要根据不同的情况让不同的类执行。个人理解,消息转发就是利用了这个中间过程。

    runtime是如何通过selector找到对应的IMP的?
    上文讲了类对象中有实例方法的列表,元类对象中有类方法的列表,列表中记录着方法的名称,参数和实现。而selector本质就是方法名称也就是SEL,通过方法名称可以在列表中找到方法实现。

    在寻找IMP的时候,runtime提供了两种方法:

    1、IMP class_getMethodImplementation(Class cls, SEL name);
    2、IMP method_getImplementation(Method m);
    对于第一种方法来说,实例方法和类方法都是调用这个方法来找到IMP,不同的是第一个参数,实例方法传的参数是[obj class];,而类方法传的参数是objc_getMetaClass("obj");
    对于第二种方法来说,传入的参数只有Method,区分类方法和实例方法在于封装Method的函数,类方法:Method class_getClassMethod(Class cls, SEL name);实例方法:Method class_getInstanceMethod(Class cls, SEL name);

    Category(objc_category)

    category是表示指向分类的一个结构体指针,结构体如下:

    struct category_t { 
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
    };
    name:是指 class_name 而不是 category_name。
    cls:要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对应到对应的类对象。
    instanceMethods:category中所有给类添加的实例方法的列表。
    classMethods:category中所有添加的类方法的列表。
    protocols:category实现的所有协议的列表。
    instanceProperties:表示Category里所有的properties,这就是我们可以通过objc_setAssociatedObject和objc_getAssociatedObject增加实例变量的原因,不过这个和一般的实例变量是不一样的。

    从上面的结构体可以看出,分类category可以添加实例方法,类方法,协议,以及通过关联对象添加属性,不可以添加成员变量。

    runtime消息转发

    前文讲到,到一个方法被执行,也就是发送消息,会去相关的方法列表中寻找对应的方法实现IMP,如果一直到根类都没找到就会进入到消息转发阶段,下面介绍一下消息转发的最后三个集会。

    1、动态方法解析
    2、备用接收者
    3、完整消息转发

    动态方法解析

    首先,当消息传递到根类都找不到方法实现时,运行时runtime会调用+resolveInstanceMethod:或者+resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数,并返回了yes,那运行时就会重新走一步消息发送的过程。

    实现一个动态方法解析的例子如下:

    - (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //执行foo函数
    [self performSelector:@selector(foo:)];
    }

    + (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(foo:)) {//如果是执行foo函数,就动态解析,指定新的IMP
    class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
    return YES;
    }
    return [super resolveInstanceMethod:sel];
    }

    void fooMethod(id obj, SEL _cmd) {
    NSLog(@"Doing foo");//新的foo函数
    }

    可以看到虽然没有实现foo这个函数,但是我们通过class_addMethod动态的添加了一个新的函数实现fooMethod,并返回了yes。

    如果返回no,就会进入下一步,- forwardingTargetForSelector:。

    备用接收者

    实现的例子如下:

    #import "ViewController.h"
    #import "objc/runtime.h"

    @interface Person: NSObject

    @end

    @implementation Person

    - (void)foo {
    NSLog(@"Doing foo");//Person的foo函数
    }

    @end

    @interface ViewController ()

    @end

    @implementation ViewController

    - (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //执行foo函数
    [self performSelector:@selector(foo)];
    }

    + (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO;//返回NO,进入下一步转发
    }

    - (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(foo)) {
    return [Person new];//返回Person对象,让Person对象接收这个消息
    }

    return [super forwardingTargetForSelector:aSelector];
    }

    @end

    可以看到我们通过-forwardingTargetForSelector:方法将当前viewController的foo函数转发给了Person的foo函数去执行了。

    如果在这一步还不能处理未知的消息,则进入下一步完整消息转发。

    完整消息转发

    首先会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nil,runtime会发出-doseNotRecognizeSelector消息,程序会挂掉;如果返回一个函数标签,runtime就会创建一个NSInvocation对象,并发送-forwardInvocation:消息给目标对象。

    实现例子如下:

    #import "ViewController.h"
    #import "objc/runtime.h"

    @interface Person: NSObject

    @end

    @implementation Person

    - (void)foo {
    NSLog(@"Doing foo");//Person的foo函数
    }

    @end

    @interface ViewController ()

    @end

    @implementation ViewController

    - (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //执行foo函数
    [self performSelector:@selector(foo)];
    }

    + (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO;//返回NO,进入下一步转发
    }

    - (id)forwardingTargetForSelector:(SEL)aSelector {
    return nil;//返回nil,进入下一步转发
    }

    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"foo"]) {
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];//签名,进入forwardInvocation
    }

    return [super methodSignatureForSelector:aSelector];
    }

    - (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;

    Person *p = [Person new];
    if([p respondsToSelector:sel]) {
    [anInvocation invokeWithTarget:p];
    }
    else {
    [self doesNotRecognizeSelector:sel];
    }

    }

    @end

    通过签名,runtime生成了一个anInvocation对象,发送给了forwardInvocation:,我们再forwardInvocation:里面让Person对象去执行了foo函数。

    以上就是runtime的三次函数转发流程。

    觉得有用,请帮忙点亮红心

    Better Late Than Never!
    努力是为了当机会来临时不会错失机会。
    共勉!

    链接:https://www.jianshu.com/p/4ae997a6c599

    收起阅读 »

    解决集成EaseIMKit源码后没有图片的问题

    经过上一篇文章如何集成环信EaseIMKit和EaseCallKit源码?之后,我们在实际使用时,会发现一个非常大的问题:就是图片都加载不出来了.这里我们可以借用easeCallKit的实现方式将EaseCallKit内的文件资源包复制一份,修改一下名,然后打...
    继续阅读 »

    经过上一篇文章如何集成环信EaseIMKit和EaseCallKit源码?之后,我们在实际使用时,会发现一个非常大的问题:

    就是图片都加载不出来了.

    这里我们可以借用easeCallKit的实现方式

    将EaseCallKit内的文件资源包复制一份,修改一下名,然后打开包,将里面的图片都替换掉,这是一个方法.

    但上述方法依然有问题,涉及到自动加载倍图问题.

    解决加载倍图也是有方法的,不过都太麻烦了,我们采用一个比较笨的方法.

    直接将EaseIMKit内的图片拖进项目内

    就像这样:



    同时,我们还需要修改加载图片的方式,项目中直接搜索:

    EaseIMKit.framework

    发现总共三个地方:







    至此已完成.

    另外我们如果使用官方demo中的代码,直接拖文件进来时,会发现好多报错.这里直接说明原因,图片重复了,搜索报错的图片名,直接保留一份即可.

    最后,再次强调:

    我们是可以采用EaseCallKit加载图片方式的,但此方式有一个非常大的问题:倍图

    (正因为尝试过并失败了,所以放弃了)

    如果我们直接采用EaseCallKit加载图片方式,不做任何处理,会自动加载一倍图,而且如果没有一倍图也不会自动加载二倍图和三倍图,我们需要手动判断和手动加载图片名后缀,比较麻烦,所以这里就偷个懒,采用上述方法来解决加载图片问题.


    收起阅读 »

    runloop 小结

    OC的两大核心runtime和runlooprunloop简介runloop本质上是一个do-while循环,当有任务处理时唤醒,没有任务时休眠,如果没有任务没有观察者的时候退出。OSX/iOS系统中,提供了两个这样的对象:NSRunLoop和CFRunLoo...
    继续阅读 »

    OC的两大核心runtime和runloop

    runloop简介

    runloop本质上是一个do-while循环,当有任务处理时唤醒,没有任务时休眠,如果没有任务没有观察者的时候退出。

    OSX/iOS系统中,提供了两个这样的对象:NSRunLoop和CFRunLoopRef.
    CFRunLoopRef是CoreFoundation框架提供的纯c的api,所有这些api都是线程安全的。
    NSRunLoop是对CFRunLoopRef的OC封装,提供了面向对象的api,这些api不是线程安全的。

    runloop和线程的关系

    首先,iOS提供了两个线程对象pthread_t和NSThread,这两个线程对象不能互相转换,但是一一对应。比如:可以通过pthread_main_thread_np()和[NSThread mainThread]获取主线程;也可以通过pthread_self()和[NSThread currentThread]获取当前线程。CFRunLoopRef是基于pthread来管理的。

    苹果不允许直接创建runloop,它只有两个获取的函数:CFRunLoopGetMain()和CFRunLoopGetCurrent()。这两个函数的内部实现大致是:

    /// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
    static CFMutableDictionaryRef loopsDic;
    /// 访问 loopsDic 时的锁
    static CFSpinLock_t loopsLock;

    /// 获取一个 pthread 对应的 RunLoop。
    CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);

    if (!loopsDic) {
    // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
    loopsDic = CFDictionaryCreateMutable();
    CFRunLoopRef mainLoop = _CFRunLoopCreate();
    CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }

    /// 直接从 Dictionary 里获取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));

    if (!loop) {
    /// 取不到时,创建一个
    loop = _CFRunLoopCreate();
    CFDictionarySetValue(loopsDic, thread, loop);
    /// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
    _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }

    OSSpinLockUnLock(&loopsLock);
    return loop;
    }

    CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
    }

    CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
    }

    可以看出来,线程和RunLoop是一一对应的,保存在一个全局的CFMutableDictionaryRef,key为pthread,value为runloop。线程刚创建时没有runloop,如果你没有主动获取,那它一直不会有。当你第一次获取runloop时,创建runloop,当线程结束时,runloop销毁。

    主线程的runloop默认开启,程序启动时,main方法,applicationMain方法内开启runloop。

    runloop的类
    在Core Foundation框架中提供了五个类关于runloop:

    1、CFRunLoopRef
    2、CFRunLoopModeRef
    3、CFRunLoopSourceRef
    4、CFRunLoopTimerRef
    5、CFRunLoopObserverRef

    它们的关系如下:


    一个runloop包含若干个Mode,一个Mode又包含若干个Source/Timer/Observer。每次调用runloop的主函数时,只能指定其中一个mode,如果想切换mode,需要退出当前runloop,再重新指定一个mode进入。这样的好处是,不同组的Source/Timer/Observer互不影响。

    CFRunLoopSourceRef是事件产生的地方。Source有两个版本,Source 0(非端口Source)和Source 1(端口Source)。

    1、Source 0 只包含一个回调函数指针,它并不能主动触发事件。使用时,需要先调用CFRunLoopSourceSignal(Source 0)将该source标记为待处理,然后手动调用CFRunLoopWakeUp()唤醒runloop,处理该事件。
    2、Source 1 包含一个mach port(端口)和一个回调的函数指针,被用于通过内核和其他线程相互发送消息。这种source能主动唤醒runloop。
    CFRunLoopTimerRef 是基于时间的触发器。其包含一个时间长度和一个回调的函数指针。当其加入到runloop时,runloop会注册对应的时间点,当时间点到时,runloop会被唤醒以执行这个回调。

    CFRunLoopObserverRef 是观察者,每个Observer都包含一个回调,当runloop的状态发生改变时,观察者可以通过回调接受到这个变化。可以接受到的状态有如下几个:

    typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
    kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
    kCFRunLoopExit = (1UL << 7), // 即将退出Loop
    };

    上述的Source/Timer/Observer被统称为一个mode item,一个item可以被加入多个mode,但一个item被重复加入同一个mode,是没有效果的。如果一个mode中一个item都没有,则runloop会自动退出。

    runloop的mode

    CFRunLoopMode和CFRunLoop的结构大致如下

    struct __CFRunLoopMode {
    CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0; // Set
    CFMutableSetRef _sources1; // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers; // Array
    ...
    };

    struct __CFRunLoop {
    CFMutableSetRef _commonModes; // Set
    CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
    CFRunLoopModeRef _currentMode; // Current Runloop Mode
    CFMutableSetRef _modes; // Set
    ...
    };

    runloop的mode包含:

    1、NSDefaultRunLoopMode:默认的mode;
    2、UITrackingRunLoopMode:跟踪用户触摸事件的mode,如UIScrollView的上下滚动;
    3、NSRunLoopCommonModes:模式集合,将一组item关联到这个模式集合上,等于将这个item关联到这个集合下的所有模式上;
    4、自定义Mode。

    这里主要解释一下NSRunLoopCommonModes,这个模式集合。
    默认NSDefaultRunLoopMode和UITrackingRunLoopMode都是包含在这个模式集合内的,当然也可以自定义一个mode,通过CFRunLoopAddCommonMode添加到这个模式集合中。

    应用场景举例:

    当一个控制器里有一个UIScrollview和一个NSTimer,UIScrollView不滚动的时候,runloop运行在NSDefaultRunLoopMode下,此时Timer会得到回调,但当UIScrollView滑动时,会将mode切换成UITrackingRunLoopMode,此时Timer得不到回调。一个解决办法就是将这个NSTimer分别绑定到NSDefaultRunLoopMode和UITrackingRunLoopMode,另一个解决办法是将这个NSTimer绑定到NSRunLoopCommonModes,两种方法都能使NSTimer在两个模式下都能得到回调。

    ps.让runloop运行在NSRunLoopCommonModes模式下是没有意思的,因为runloop一个时间只能运行在一个模式下。

    端口Source通信的步骤
    demo如下:

    - (void)testDemo3
    {
    //声明两个端口 随便怎么写创建方法,返回的总是一个NSMachPort实例
    NSMachPort *mainPort = [[NSMachPort alloc]init];
    NSPort *threadPort = [NSMachPort port];
    //设置线程的端口的代理回调为自己
    threadPort.delegate = self;

    //给主线程runloop加一个端口
    [[NSRunLoop currentRunLoop]addPort:mainPort forMode:NSDefaultRunLoopMode];

    dispatch_async(dispatch_get_global_queue(0, 0), ^{

    //添加一个Port
    [[NSRunLoop currentRunLoop]addPort:threadPort forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop]runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

    });

    NSString *s1 = @"hello";

    NSData *data = [s1 dataUsingEncoding:NSUTF8StringEncoding];

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSMutableArray *array = [NSMutableArray arrayWithArray:@[mainPort,data]];
    //过2秒向threadPort发送一条消息,第一个参数:发送时间。msgid 消息标识。
    //components,发送消息附带参数。reserved:为头部预留的字节数(从官方文档上看到的,猜测可能是类似请求头的东西...)
    [threadPort sendBeforeDate:[NSDate date] msgid:1000 components:array from:mainPort reserved:0];

    });

    }

    //这个NSMachPort收到消息的回调,注意这个参数,可以先给一个id。如果用文档里的NSPortMessage会发现无法取值
    - (void)handlePortMessage:(id)message
    {

    NSLog(@"收到消息了,线程为:%@",[NSThread currentThread]);

    //只能用KVC的方式取值
    NSArray *array = [message valueForKeyPath:@"components"];

    NSData *data = array[1];
    NSString *s1 = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"%@",s1);

    // NSMachPort *localPort = [message valueForKeyPath:@"localPort"];
    // NSMachPort *remotePort = [message valueForKeyPath:@"remotePort"];

    }

    声明两个端口,sendPort,receivePort,设置receivePort的代理,分别将sendPort和receivePort绑定到两个线程的自己的runloop上,然后回到发送线程用接收端口发送数据([threadPort sendBeforeDate:[NSDate date] msgid:1000 components:array from:mainPort reserved:0]; from参数标注从发送端口发出),注意这里发送的数据格式为array,内容格式只能为NSPort或者NSData,在代理方法- (void)handlePortMessage:(id)message中接收数据;

    RunLoop的内部实现


    内部代码整理,不想看可以跳过,看下方总结:

    /// 用DefaultMode启动
    void CFRunLoopRun(void) {
    CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
    }

    /// 用指定的Mode启动,允许设置RunLoop超时时间
    int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
    }

    /// RunLoop的实现
    int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {

    /// 首先根据modeName找到对应mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
    /// 如果mode里没有source/timer/observer, 直接返回。
    if (__CFRunLoopModeIsEmpty(currentMode)) return;

    /// 1. 通知 Observers: RunLoop 即将进入 loop。
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);

    /// 内部函数,进入loop
    __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {

    Boolean sourceHandledThisLoop = NO;
    int retVal = 0;
    do {

    /// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
    /// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
    /// 执行被加入的block
    __CFRunLoopDoBlocks(runloop, currentMode);

    /// 4. RunLoop 触发 Source0 (非port) 回调。
    sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
    /// 执行被加入的block
    __CFRunLoopDoBlocks(runloop, currentMode);

    /// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
    if (__Source0DidDispatchPortLastTime) {
    Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
    if (hasMsg) goto handle_msg;
    }

    /// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
    if (!sourceHandledThisLoop) {
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
    }

    /// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
    /// • 一个基于 port 的Source 的事件。
    /// • 一个 Timer 到时间了
    /// • RunLoop 自身的超时时间到了
    /// • 被其他什么调用者手动唤醒
    __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
    mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
    }

    /// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);

    /// 收到消息,处理消息。
    handle_msg:

    /// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
    if (msg_is_timer) {
    __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
    }

    /// 9.2 如果有dispatch到main_queue的block,执行block。
    else if (msg_is_dispatch) {
    __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
    }

    /// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
    else {
    CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
    sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
    if (sourceHandledThisLoop) {
    mach_msg(reply, MACH_SEND_MSG, reply);
    }
    }

    /// 执行加入到Loop的block
    __CFRunLoopDoBlocks(runloop, currentMode);


    if (sourceHandledThisLoop && stopAfterHandle) {
    /// 进入loop时参数说处理完事件就返回。
    retVal = kCFRunLoopRunHandledSource;
    } else if (timeout) {
    /// 超出传入参数标记的超时时间了
    retVal = kCFRunLoopRunTimedOut;
    } else if (__CFRunLoopIsStopped(runloop)) {
    /// 被外部调用者强制停止了
    retVal = kCFRunLoopRunStopped;
    } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
    /// source/timer/observer一个都没有了
    retVal = kCFRunLoopRunFinished;
    }

    /// 如果没超时,mode里没空,loop也没被停止,那继续loop。
    } while (retVal == 0);
    }

    /// 10. 通知 Observers: RunLoop 即将退出。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    }

    可以看到,实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

    runloop的运行逻辑:

    1、通知监听者,即将进入runloop;
    2、通知监听者,将要处理Timer;
    3、通知监听者,将要处理Source0(非端口InputSource);
    4、处理Source0;
    5、如果有Source1,跳到第9步;
    6、通知监听者,线程即将进入休眠;
    7、runloop进入休眠,等待唤醒;
       1.source0;
       2.Timer启动;
       3.外部手动唤醒
    8、通知监听者,线程将被唤醒;
    9、处理未处理的任务;
       1.如果用户定义的定时器任务启动,处理定时器任务并重启runloop,进入步骤2;
       2.如果输入源启动,传递相应的消息;
       3.如果runloop被显示唤醒,且没有超过设置的时间,重启runloop,进入步骤2;
    10、通知监听者,runloop结束。
       1.runloop结束,没有timer或者没有source;
       2.runloop被停止,使用CFRunloopStop停止Runloop;
       3.runloop超时;
       4.runloop处理完事件。

    苹果用runloop实现的功能

    1、自动释放池,在主程序启动时,再即将进入runloop的时候会执行autoreleasepush(),新建一个autoreleasePoolPage,同时push一个哨兵对象到这个page中;当runloop进入休眠模式时,会执行autoreleasepop(),释放旧池,同时autoreleasepush(),创建新池;当runloop退出时,清空自动释放池。

    2、定时器NSTimer实际上就是CFRunloopTimerRef。

    觉得有用,请帮忙点亮红心

    Better Late Than Never!
    努力是为了当机会来临时不会错失机会。
    共勉!

    链接:https://www.jianshu.com/p/8fdda9f64459

    收起阅读 »

    如何集成环信EaseIMKit和EaseCallKit源码?

    EaseIMKit是一个基于环信sdk的UI库,封装了IM功能常用的控件、fragment等等。官网下载源码EaseCallKit源码EaseIMKit源码第二步 & 第三步整理一份路径 & 整理EaseCallKit文件及文件夹 ...
    继续阅读 »

    EaseIMKit是一个基于环信sdk的UI库,封装了IM功能常用的控件、fragment等等。

    下面给大家分享一下如何引入EaseIMKit源码

    第一步

    官网下载源码

    源码从这里找:环信开源GitHub


    EaseCallKit源码



    EaseIMKit源码


    第二步 & 第三步

    整理一份路径 & 整理EaseCallKit文件及文件夹
    [注意!!!注意!!!注意!!! 上下两个文件夹窗口内有同名文件夹,因为就是同一个文件夹.这里专门开了两个窗口,为了更加直观!!!比如"00刚下载的源码/01整理之后的内容/02展示项目"这三个文件夹上下窗口的文件夹是同一个路径的文件夹]



    第四步
    整理EaseIMKit文件及文件夹



    第五步
    修改两个文件
    (EaseIMKit.podspec & EaseCallKit.podspec)
    (两个文件内容:文章末尾有文本内容可直接复制)



    第六步
    创建项目
    这里创建项目名叫EaseSourceCode

    将整理好内容的源码放入项目文件夹内,创建podfile,podfile内容如下

    (podfile内容:文章末尾有文本内容可直接复制)



    第七步
    pod install后运行项目
    先command+b编译,再引入头文件



    第八步

    最后集成完成,你会发现没有图片!下一篇文章将会讲解如何将图片加载出来.

    附:

    EaseIMKit.podspec内容如下

    #=====================================

    Pod::Spec.new do |s|

      s.name = 'EaseIMKit'

      s.version = '3.8.1.1'

      s.summary = 'easemob im sdk UIKit'

      s.description = <<-DESC

            EaseMob YES!!!

      DESC

      s.homepage = 'http://docs-im.easemob.com/im/ios/other/easeimkit'

      s.license          = 'MIT'

      s.platform = :ios, '10.0'

      s.author = { 'easemob' => 'dev@easemob.com' }

      s.source = { :git => 'http://XXX/EaseIMKit.git', :tag => s.version.to_s }

      s.frameworks = 'UIKit'

      s.libraries = 'stdc++'

      s.source_files = 'Class/**/*.{h,m,mm}'

      s.requires_arc = true

      s.resources = 'Class/EaseIMImage.bundle'

      s.static_framework = true

      s.dependency 'EMVoiceConvert', '~> 0.1.0'

      s.dependency 'HyphenateChat'

    end

    #=====================================

    EaseCallKit.podspec内容如下

    #=====================================

    Pod::Spec.new do |s|

        s.name            ='EaseCallKit'

        s.version          ='3.8.1.1'

        s.summary          ='A UI framework with video and audio call'

        s.description      = <<-DESC

            EaseMob YES!!!

        DESC

        s.license          ='MIT'

        s.homepage ='https://www.easemob.com'

        s.author          = {'easemob'=>'dev@easemob.com'}

        s.source          = { :git =>'http://XXX/EaseCallKit.git', :tag => s.version.to_s }

        s.frameworks ='UIKit'

        s.libraries ='stdc++'

        s.ios.deployment_target ='9.0'

        s.source_files ='Classes/**/*.{h,m}'

        s.requires_arc =true

        s.resources ='Assets/EaseCall.bundle'

        s.dependency'HyphenateChat'

        s.dependency'Masonry'

        s.dependency'AgoraRtcEngine_iOS'

        s.dependency'SDWebImage'

    end

    #=====================================

    podfile文件内容如下

    #=====================================

    # platform :ios, '9.0'

    use_frameworks!

    target 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaa' do

        pod'MBProgressHUD'

        pod'SDWebImage'

        pod'Masonry'

        pod'MJRefresh'

        pod'HyphenateChat'

        pod'AgoraRtcEngine_iOS' 

        pod'EaseIMKit', :path => './localPodsLibrary/EaseIMKit'

        pod'EaseCallKit',  :path =>'./localPodsLibrary/EaseCallKit'


    end

    #=====================================

    下一篇:

    解决集成EaseIMKit源码后没有图片的问题

    收起阅读 »

    iOS离屏渲染的触发原理与躲在背后的性能优化

    一.带着问题了解什么是离屏渲染?        在iOS开发中,我们经常会写到这样的代码:btn.layer.cornerRadius = 50;btn.clipsToBounds = YE...
    继续阅读 »

    一.带着问题了解什么是离屏渲染?

            在iOS开发中,我们经常会写到这样的代码:btn.layer.cornerRadius = 50;btn.clipsToBounds = YES;很多的面试官也会问我们平常给VIew设置圆角的时候应该注意什么?在UITableViewCell中,如果出现了btn.layer.cornerRadius = 50;btn.clipsToBounds = YES这两行代码,会给tableVIew的渲染以及滑动带来什么影响?为什么在有些地方不建议使用这样的代码设置圆角?(btn只是一个举例,实际上它可以是UIview,UIbutton,uiimageVIew等),你们是否能回答出面试官心中想要的答案?

    二.离屏渲染的由来

            在上一篇文章中,我提到了图像/图形渲染的流程:GPU进⾏渲染->帧缓存区⾥ ->视频控制器->读取帧缓存区信息(位图) -> 数模转化(数字信号处->模 拟型号) ->(逐⾏扫描)显示,重点来了:当帧缓冲区的数据不能直接被视频控制器扫描显示的时候,我们要额外的开辟一个缓冲区------->离屏缓冲区来存储我们不能第一时间交给视频控制器显示的数据,在离屏缓冲区渲染好我们不能直接被视频控制器显示的数据,等到最终我们可以确认当前的VIew到底怎么显示之后,再交给帧缓冲区----->视频控制器显示。




    离屏渲染的代价很高,想要进行离屏渲染,首选要创建一个新的缓冲区,屏幕渲染会有一个上下文环境的一个概念,离屏渲染的整个过程需要切换上下文环境,先从当前屏幕切换到离屏,等结束后,又要将上下文环境切换回来。这也是为什么会消耗性能的原因了 (间接回答了如果出现了btn.layer.cornerRadius = 50;btn.clipsToBounds = YES这两行代码,会给tableVIew的渲染以及滑动带来什么影响?)   

            特别提醒:离屏渲染是系统触发,触发了之后才有离屏缓冲区,离屏缓冲区和我上一篇文章讲到的帧缓冲区的二级缓冲机制没有任何的因果关系

            最终当触发了离屏渲染之后,图像/图形的渲染流程变成了:app进⾏额外的渲染和合并-> offscreen Buffer(离屏缓冲区) 组合. -> FrameBuffer(帧缓冲区) -> 屏幕;特点:(离屏渲染-> 额外的存储空间/offscreen Buffer->FrameBuffer ) offscreenBuffer 空间大小-> 屏幕像素点2.5倍 


    离屏渲染遵循画家算法:按次序输出到frame buffer,后一层覆盖前一层,就能得到最终的显示结果(值得一提的是,与一般桌面架构不同,在iOS中,设备主存和GPU的显存共享物理内存,这样可以省去一些数据传输开销),然而有些场景并没有那么简单。作为“画家”的GPU虽然可以一层一层往画布上进行输出,但是无法在某一层渲染完成之后,再回过头来擦除/改变其中的某个部分——因为在这一层之前的若干层layer像素数据,已经在渲染中被永久覆盖了。这就意味着,对于每一层layer,要么能找到一种通过单次遍历就能完成渲染的算法,要么就不得不另开一块内存,借助这个临时中转区域来完成一些更复杂的、多次的修改/剪裁操作

    三.btn.layer.cornerRadius = 50&&btn.clipsToBounds = YES就一定会触发离屏渲染?

            首先我们先开启离屏渲染的检测,在模拟器打开color offscreen-rendered,开启后会把那些需要离屏渲染



    这里就明显看出1和3变成了黄色,标记为触发了离屏渲染,个人觉得这应该是模拟器的bug吧,如果你的电脑没有出现这个问题,请忽略,有的话就试着选一选其他机型吧!!!

    首先普及一下CALayer的层次结构:CALayer由背景色backgroundColor、内容contents、边缘borderWidth&borderColor构成

    重点重点重点(重要的事情说三遍):cornerRadius的文档中明确说明对cornerRadius的设置只对 CALayer 的backgroundColor和borderWidth&borderColor起作用,如果contents有内容或者内容的背景不是透明的话,只有设置masksToBounds为 true 才能起作用,此时两个属性相结合,产生离屏渲染。这也就说明了上面代码为什么1和3触发了离屏渲染,而2和4没有触发离屏渲染

    解决办法:

    (1)后台绘制圆角图片,前台进行设置




    (2)对于 contents 无内容或者内容的背景透明(无涉及到圆角以外的区域)的layer,直接设置layer的 backgroundColor 和 cornerRadius 属性来绘制圆角。

    (3)使用混合图层,在layer上方叠加相应mask形状的半透明layer

    sublayer.contents=(id)[UIImage imageNamed:@"xxx"].CGImage;

    [view.layer addSublayer:sublayer];

    (4)- (UIImage *)yy_imageByRoundCornerRadius:(CGFloat)radius corners:(UIRectCorner)corners borderWidth:(CGFloat)borderWidth borderColor:(UIColor *)borderColor borderLineJoin:(CGLineJoin)borderLineJoin此方法为YY_image处理圆角的方法,你可以去下载YY_image查看源码

    其他情况触发离屏渲染以及解决办法:

    1. mask(遮罩)------>使用混合图层,在layer上方叠加相应mask形状的半透明layer

    2.edge antialiasing(抗锯齿)----->不设置 allowsEdgeAntialiasing 属性为YES(默认为NO)

    3. allowsGroupOpacity(组不透明,开启CALayer的allowsGroupOpacity属性后,子 layer 在视觉上的透明度的上限是其父 layer 的opacity(对应UIView的alpha),并且从 iOS 7 以后默认全局开启了这个功能,这样做是为了让子视图与其容器视图保持同样的透明度。)------->关闭 allowsGroupOpacity 属性,按产品需求自己控制layer透明度

    4.shadows(阴影)------>设置阴影后,设置CALayer的 shadowPath,view.layer.shadowPath=[UIBezierPath pathWithCGRect:view.bounds].CGPath;

    CALayer离屏渲染终极解决方案:当视图内容是静态不变时,设置 shouldRasterize(光栅化)为YES(缓存离屏渲染的数据,当下次用到的时候直接拿,不需要开辟新的离屏缓冲区),此方案最为实用方便。view.layer.shouldRasterize = true;view.layer.rasterizationScale = view.layer.contentsScale;

    shouldRasterize (光栅华使用建议):

    1.如果layer不需要服用,则没有必要打开

    2.如果layer不是静态的,需要被频繁修改,比如出于动画之中,则开启光栅华反而影响性能

    3.离屏渲染缓存有时间限制,当超过100ms,内容没有被使用就会被丢弃,无法复用

    4.离屏渲染缓存有空间限制,超过屏幕像素的2.5倍则失效,并无法使用

    特别说明:当视图内容是动态变化(如后台下载图片完毕后切换到主线程设置)时,使用此方案反而为增加系统负荷。

    总结:

    (1)离屏渲染是系统触发,触发了之后才有离屏缓冲区,离屏缓冲区和我上一篇文章讲到的帧缓冲区的二级缓冲机制没有任何的因果关系

    (2)btn.layer.cornerRadius = 50&&btn.clipsToBounds = YES不一定会触发离屏渲染,cornerRadius的文档中明确说明对cornerRadius的设置只对 CALayer 的backgroundColor和borderWidth&borderColor起作用,如果contents有内容或者内容的背景不是透明的话,只有设置masksToBounds为 true 才能起作用,此时两个属性相结合,产生离屏渲染

    (3)在uitableVIewcell触发了离屏渲染,会导致在滑动的时候高频率的开辟离屏缓冲区,这样就会造成tanleView滑动卡顿,如果视图内容是静态不变时,设置 shouldRasterize(光栅化)为YES,此方案最为实用方便,但是当视图内容是动态变化(如后台下载图片完毕后切换到主线程设置)时,使用此方案反而为增加系统负荷。

    (4)现在摆在我们面前得有三个选择:当前屏幕渲染、离屏渲染、CPU渲染,该用哪个呢?这需要根据具体的使用场景来决定。·   尽量使用当前屏幕渲染,鉴于离屏渲染、CPU渲染可能带来的性能问题,一般情况下,我们要尽量使用当前屏幕渲染。离屏渲染 VS CPU渲染



    作者:枫紫
    链接:https://www.jianshu.com/p/3448d19c3495









    收起阅读 »

    iOS------OpenGL 图形专有名词与坐标解析

    一.OpenGL简介OpenGL(英语:Open Graphics Library,译名:开放图形库或者“开放式图形库”)是用于渲染2D、3D矢量图形的跨语言、跨平台的应用程序编程接口(API)。它将计算机的资源抽象称为⼀个个OpenGL的对象,对这些资源的操...
    继续阅读 »

    一.OpenGL简介

    OpenGL(英语:Open Graphics Library,译名:开放图形库或者“开放式图形库”)是用于渲染2D3D矢量图形的跨语言跨平台应用程序编程接口(API)。它将计算机的资源抽象称为⼀个个OpenGL的对象,对这些资源的操作抽象为⼀个个的OpenGL指令,开发者可以在mac程序中使用OpenGl来实现图形渲染。图形API的目的就是实现图形的底层渲染,比如游戏场景/游戏人物的渲染,音视频解码后数据的渲染,地图上的渲染,动画绘制等。在iOS开发中,开发者唯一能够GPU的就是图形API。(GPU---图形处理器(英语:Graphics Processing Unit,缩写:GPU),又称显示核心、视觉处理器、显示芯片,是一种专门在个人电脑工作站、游戏机和一些移动设备(如平板电脑智能手机等)上做图像和图形相关运算工作的微处理器。)

    二.OpenGL专业名词解析

        1.OpenGL 上下⽂( context )

            OpenGL Context,中文解释就是OpenGL的上下文,因为OpenGL没有窗口的支持,我们在使用OpenGl的时候,一般是在main函数创建窗口: 

            //GLUT窗口大小、窗口标题

            glutInitWindowSize(800, 600);

            glutCreateWindow("Triangle");

            然后我们在创建的窗口里面绘制,个人理解上下文的意思就是指的是OpenGL的作用范围,当然OpenGL的Context不只是这个窗口,这个窗口我们可以理解为OpenGL的default framebuffer,所以Context还包含关于这个framebuffer的一些参数设置信息,具体内容可以查看OpenGL的Context的结构体,Context记录了OpenGL渲染需要的所有信息,它是一个大的结构体,它里面记录了当前绘制使用的颜色、是否有光照计算以及开启的光源等非常多我们使用OpenGL函数调用设置的状态和状态属性等等,你可以把它理解为是一个巨大的状态机,它里面保存OpenGl的指令,在图形渲染的时候,可以理解为这个状态机开始工作了,对某个属性或者开关发出指令。它的特点就是:有记忆功能,接收输入,根据输入的指令,修改当前的状态,并且可以输出内容,当停机的时候不再接收指令。

    2.渲染

            渲染就是把数据显示到屏幕上,在OpenGl中,渲染指的是将图形/图像数据转换为2D空间图像操作叫渲染

    3.顶点数组/顶点缓冲区

            在OpenGL中,基本图元有三种:点,线,三角形,复杂的图形由这三种图元组成,我们在画点/线/三角形的时候是不是应该先知道每个顶点的坐标,而这些坐标放在数组里,就叫顶点数组。顶点数组存在内存当中,但是为了提高性能,提前分配一块显存,将顶点数组预先存入到显存当中,这部分的显存就叫顶点缓冲区。

    4.着色器(shader)

            为什么要使用着色器?我们知道,OpenGL一般使用经典的固定渲染管线来渲染对象,OpenGL在实际调⽤绘制函数之前,还需指定⼀个由shader编译成的着⾊器程序。常⻅的着⾊器主要有顶点着⾊器(VertexShader),⽚段着⾊器(FragmentShader)/像素着⾊器(PixelShader),⼏何着⾊器(GeometryShader),曲⾯细分着⾊器(TessellationShader)。⽚段着⾊器和像素着⾊器只是在OpenGL和DX中的不同叫法⽽已。可惜的是,直到OpenGLES 3.0,依然只⽀持了顶点着⾊器和⽚段着⾊器这两个最基础的着⾊器。OpenGL在处理shader时,和其他编译器⼀样。通过编译、链接等步骤,⽣成了着⾊器程序(glProgram),着⾊器程序同时包含了顶点着⾊器和⽚段着⾊器的运算逻辑。在OpenGL进⾏绘制的时候,⾸先由顶点着⾊器对传⼊的顶点数据进⾏运算。再通过图元装配,将顶点转换为图元。然后进⾏光栅化,将图元这种⽮量图形,转换为栅格化数据。最后,将栅格化数据传⼊⽚段着⾊器中进⾏运算。⽚段着⾊器会对栅格化数据中的每⼀个像素进⾏运算,并决定像素的颜⾊(顶点着色器和片段/片元着色器会在下面讲解)

    5.管线

            OpenGL在渲染图形/图像的时候是按照特定的顺序来执行的,不能修改打破,管线的意思个人理解是读取顶点数据—>顶点着色器—>组装图元—>光栅化图元—>片元着色器—>写入帧缓冲区—>显示到屏幕上,类似这样的流水线,当图像/图形显示到屏幕上,这一条管线完成工作。下面是分步讲解:

           (1)读取顶点数据指的是将待绘制的图形的顶点数据传递给渲染管线中。

            (2)顶点着色器最终生成每个定点的最终位置,执行顶点的各种变换,它会针对每个顶点执行一次,确定了最终位置后,OpenGL就可以把这些顶点集合按照给定的参数类型组装成点,线或者三角形。

          (3)组装图元阶段包括两部分:图元的组装和图元处理,图元组装指的是顶点数据根据设置的绘制方式参数结合成完整的图元,例如点绘制方式中每个图元就只包含一个点,线段绘制方式中每个图源包含两个点;图元处理主要是剪裁以使得图元位于视景体内部的部分传递到下一个步骤,视景体外部的部分进行剪裁。视景体的概念与投影有关。

          (4)光栅化图元主要指的是将一个图元离散化成可显示的二维单元片段,这些小单元称为片元。一个片元对应了屏幕上的一个或多个像素,片元包括了位置,颜色,纹理坐标等信息,这些值是由图元的顶点信息进行插值计算得到的。

          (5)片元着色器为每个片元生成最终的颜色,针对每个片元都会执行一次。一旦每个片元的颜色确定了,OpenGL就会把它们写入到帧缓冲区中。

    6.顶点着色器

             • ⼀般⽤来处理图形每个顶点变换(旋转/平移/投影等)                     

            • 顶点着⾊器是OpenGL中⽤于计算顶点属性的程序。顶点着⾊器是逐顶点运算的程序,也就是说每个顶点数据都会执⾏⼀次顶点着⾊器,当然这是并⾏的,并且顶点着⾊器运算过程中⽆法访问其他顶点的数据

            • ⼀般来说典型的需要计算的顶点属性主要包括顶点坐标变换、逐顶点光照运算等等。顶点坐标由⾃身坐标系转换到归⼀化坐标系的运算,就是在这⾥发⽣的。

    7.片元着色器(片段着色器)

            ⼀般⽤来处理图形中每个像素点颜⾊计算和填充⽚段着⾊器是OpenGL中⽤于计算⽚段(像素)颜⾊的程序。⽚段着⾊器是逐像素运算的程序,也就是说每个像素都会执⾏⼀次⽚段着⾊器,当然也是并⾏的

    8.光栅化Rasterization 

            • 是把顶点数据转换为⽚元的过程,具有将图转化为⼀个个栅格组成的图象的作⽤,特点是每个元素对应帧缓冲区中的⼀像素。

            • 光栅化就是把顶点数据转换为⽚元的过程。⽚元中的每⼀个元素对应于帧缓冲区中的⼀个像素。

            • 光栅化其实是⼀种将⼏何图元变为⼆维图像的过程。该过程包含了两部分的⼯作。第⼀部分⼯作:决定窗⼝坐标中的哪些整型栅格区域被基本图元占⽤;第⼆部分⼯作:分配⼀个颜⾊值和⼀个深度值到各个区域。光栅化过程产⽣的是⽚元

            • 把物体的数学描述以及与物体相关的颜⾊信息转换为屏幕上⽤于对应位置的像素及⽤于填充像素的颜⾊,这个过程称为光栅化,这是⼀个将模拟信号转化为离散信号的过程

    9.纹理

            纹理可以理解为图⽚. ⼤家在渲染图形时需要在其编码填充图⽚,为了使得场景更加逼真.⽽这⾥使⽤的图⽚,就是常说的纹理.但是在OpenGL,我们更加习惯叫纹理,⽽不是图⽚

    10.混合(Blending)

            在测试阶段之后,如果像素依然没有被剔除,那么像素的颜⾊将会和帧缓冲区中颜⾊附着上的颜⾊进⾏混合,混合的算法可以通过OpenGL的函数进⾏指定。但是OpenGL提供的混合算法是有限的,如果需要更加复杂的混合算法,⼀般可以通过像素着⾊器进⾏实现,当然性能会⽐原⽣的混合算法差⼀些,个人理解有点像iOS给RGB中红,绿,蓝设置不同的值得到不同的颜色,只是这里是操作片元着色器,来达到不同的显示。

    11.变换矩阵(Transformation)/投影矩阵Projection 

            在iOS核心动画中我们也会和矩阵打交到,变换矩阵顾名思义就是对图像/图形的放大/缩小/平移/选装等座处理。

            投影矩阵就是⽤于将3D坐标转换为⼆维屏幕坐标,实际线条也将在⼆维坐标下进⾏绘制    

    12.渲染上屏/交换缓冲区(SwapBuffer)     

        • 渲染缓冲区⼀般映射的是系统的资源⽐如窗⼝。如果将图像直接渲染到窗⼝对应的渲染缓冲区,则可以将图像显示到屏幕上。

        • 但是,值得注意的是,如果每个窗⼝只有⼀个缓冲区,那么在绘制过程中屏幕进⾏了刷新,窗⼝可能显示出不完整的图像

        • 为了解决这个问题,常规的OpenGL程序⾄少都会有两个缓冲区。显示在屏幕上的称为屏幕缓冲区,没有显示的称为离屏缓冲区。在⼀个缓冲区渲染完成之后,通过将屏幕缓冲区和离屏缓冲区交换,实现图像在屏幕上的显示。

        • 由于显示器的刷新⼀般是逐⾏进⾏的,因此为了防⽌交换缓冲区的时候屏幕上下区域的图像分属于两个不同的帧,因此交换⼀般会等待显示器刷新完成的信号,在显示器两次刷新的间隔中进⾏交换,这个信号就被称为垂直同步信号,这个技术被称为垂直同步

        • 使⽤了双缓冲区和垂直同步技术之后,由于总是要等待缓冲区交换之后再进⾏下⼀帧的渲染,使得帧率⽆法完全达到硬件允许的最⾼⽔平。为了解决这个问题,引⼊了三缓冲区技术,在等待垂直同步时,来回交替渲染两个离屏的缓冲区,⽽垂直同步发⽣时,屏幕缓冲区和最近渲染完成的离屏缓冲区交换,实现充分利⽤硬件性能的⽬的

    13.坐标系

          OpenGl常见的坐标系有:

            1. Object or model coordinates(物体或模型坐标系)每一个实物都有自己的坐标系,在高中数学中,以自身建立的坐标系,自身坐标系由世界坐标系平移而来

            2. World coordinates(世界坐标系)个人理解为地球相对自己建立的坐标系,地球上所有生物都处于这个坐标系当中

            3. Eye (or Camera) coordinates(眼(或相机)坐标系)

            4. Normalized device coordinates(标准化的设备坐标系)

            5. Window (or screen) coordinates(.窗口(或屏幕)坐标系)个人理解为iOS下

            6.Clip coordinates(裁剪坐标系)主要作用是当图形/图像超出时,按照这个坐标系裁剪,裁剪好之后转换到screen坐标系

    14.正投影/透视投影

            正投影:类似于照镜子,1:1形成图形大小,这里不做重点讲解

            透视投影:在OpenGL中,如果想对模型进行操作,就要对这个模型的状态(当前的矩阵)乘上这个操作对应的一个矩阵.如果乘以变换矩阵(平移, 缩放, 旋转), 那相乘之后, 模型的位置被变换;如果乘以投影矩阵(将3D物体投影到2D平面), 相乘后, 模型的投影方式被设置;如果乘以纹理矩阵(), 模型的纹理方式被设置.而用来指定乘以什么类型的矩阵, 就是glMatriMode(GLenummode);glMatrixMode有3种模式: GL_PROJECTION 投影, GL_MODELVIEW 模型视图, GL_TEXTURE 纹理.所以,在操作投影矩阵以前,需要调用函数:glMatrixMode(GL_PROJECTION); //将当前矩阵指定为投影矩阵然后把矩阵设为单位矩阵






    作者:枫紫
    链接:https://www.jianshu.com/p/03d3a5ab2db0

    收起阅读 »

    一行代码集成Android推送!一个轻量级、可插拔的Android消息推送框架。

    快速集成指南添加Gradle依赖1.先在项目根目录的 build.gradle 的 repositories 添加:allprojects { repositories { ... maven { url "https:...
    继续阅读 »

    快速集成指南

    添加Gradle依赖

    1.先在项目根目录的 build.gradle 的 repositories 添加:

    allprojects {
    repositories {
    ...
    maven { url "https://jitpack.io" }
    }
    }

    2.添加XPush主要依赖:

    dependencies {
    ...
    //推送核心库
    implementation 'com.github.xuexiangjys.XPush:xpush-core:1.0.1'
    //推送保活库
    implementation 'com.github.xuexiangjys.XPush:keeplive:1.0.1'
    }

    3.添加第三方推送依赖(根据自己的需求进行添加,当然也可以全部添加)

    dependencies {
    ...
    //选择你想要集成的推送库
    implementation 'com.github.xuexiangjys.XPush:xpush-jpush:1.0.1'
    implementation 'com.github.xuexiangjys.XPush:xpush-umeng:1.0.1'
    implementation 'com.github.xuexiangjys.XPush:xpush-huawei:1.0.1'
    implementation 'com.github.xuexiangjys.XPush:xpush-xiaomi:1.0.1'
    implementation 'com.github.xuexiangjys.XPush:xpush-xg:1.0.1'
    }

    初始化XPush配置

    1.注册消息推送接收器。方法有两种,选其中一种就行了。

    • 如果你想使用XPushManager提供的消息管理,直接在AndroidManifest.xml中注册框架默认提供的XPushReceiver。当然你也可以继承XPushReceiver,并重写相关方法。

    • 如果你想实现自己的消息管理,可继承AbstractPushReceiver类,重写里面的方法,并在AndroidManifest.xml中注册。

        <!--自定义消息推送接收器-->
    <receiver android:name=".push.CustomPushReceiver">
    <intent-filter>
    <action android:name="com.xuexiang.xpush.core.action.RECEIVE_CONNECT_STATUS_CHANGED" />
    <action android:name="com.xuexiang.xpush.core.action.RECEIVE_NOTIFICATION" />
    <action android:name="com.xuexiang.xpush.core.action.RECEIVE_NOTIFICATION_CLICK" />
    <action android:name="com.xuexiang.xpush.core.action.RECEIVE_MESSAGE" />
    <action android:name="com.xuexiang.xpush.core.action.RECEIVE_COMMAND_RESULT" />

    <category android:name="${applicationId}" />
    </intent-filter>
    </receiver>

    <!--默认的消息推送接收器-->
    <receiver android:name="com.xuexiang.xpush.core.receiver.impl.XPushReceiver">
    <intent-filter>
    <action android:name="com.xuexiang.xpush.core.action.RECEIVE_CONNECT_STATUS_CHANGED" />
    <action android:name="com.xuexiang.xpush.core.action.RECEIVE_NOTIFICATION" />
    <action android:name="com.xuexiang.xpush.core.action.RECEIVE_NOTIFICATION_CLICK" />
    <action android:name="com.xuexiang.xpush.core.action.RECEIVE_MESSAGE" />
    <action android:name="com.xuexiang.xpush.core.action.RECEIVE_COMMAND_RESULT" />

    <category android:name="${applicationId}" />
    </intent-filter>
    </receiver>

    注意,如果你的Android设备是8.0及以上的话,静态注册的广播是无法正常生效的,解决的方法有两种:

    • 动态注册消息推送接收器

    • 修改推送消息的发射器

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    //Android8.0静态广播注册失败解决方案一:动态注册
    XPush.registerPushReceiver(new CustomPushReceiver());

    //Android8.0静态广播注册失败解决方案二:修改发射器
    XPush.setIPushDispatcher(new Android26PushDispatcherImpl(CustomPushReceiver.class));
    }

    2.在AndroidManifest.xml的application标签下,添加第三方推送客户端实现类.

    需要注意的是,这里注册的PlatformNamePlatformCode必须要和推送客户端实现类中的一一对应才行。

    <!--name格式:XPush_[PlatformName]_[PlatformCode]-->
    <!--value格式:对应客户端实体类的全类名路径-->

    <!--如果引入了xpush-jpush库-->
    <meta-data
    android:name="XPush_JPush_1000"
    android:value="com.xuexiang.xpush.jpush.JPushClient" />

    <!--如果引入了xpush-umeng库-->
    <meta-data
    android:name="XPush_UMengPush_1001"
    android:value="com.xuexiang.xpush.umeng.UMengPushClient" />

    <!--如果引入了xpush-huawei库-->
    <meta-data
    android:name="XPush_HuaweiPush_1002"
    android:value="com.xuexiang.xpush.huawei.HuaweiPushClient" />

    <!--如果引入了xpush-xiaomi库-->
    <meta-data
    android:name="XPush_MIPush_1003"
    android:value="com.xuexiang.xpush.xiaomi.XiaoMiPushClient" />

    <!--如果引入了xpush-xg库-->
    <meta-data
    android:name="XPush_XGPush_1004"
    android:value="@string/xpush_xg_client_name" />

    3.添加第三方AppKey和AppSecret.

    这里的AppKey和AppSecret需要我们到各自的推送平台上注册应用后获得。注意如果使用了xpush-xiaomi,那么需要在AndroidManifest.xml添加小米的AppKey和AppSecret(注意下面的“\ ”必须加上,否则获取到的是float而不是String,就会导致id和key获取不到正确的数据)。

    <!--极光推送静态注册-->
    <meta-data
    android:name="JPUSH_CHANNEL"
    android:value="default_developer" />
    <meta-data
    android:name="JPUSH_APPKEY"
    android:value="a32109db64ebe04e2430bb01" />

    <!--友盟推送静态注册-->
    <meta-data
    android:name="UMENG_APPKEY"
    android:value="5d5a42ce570df37e850002e9" />
    <meta-data
    android:name="UMENG_MESSAGE_SECRET"
    android:value="4783a04255ed93ff675aca69312546f4" />

    <!--华为HMS推送静态注册-->
    <meta-data
    android:name="com.huawei.hms.client.appid"
    android:value="101049475"/>

    <!--小米推送静态注册,下面的“\ ”必须加上,否则将无法正确读取-->
    <meta-data
    android:name="MIPUSH_APPID"
    android:value="\ 2882303761518134164"/>
    <meta-data
    android:name="MIPUSH_APPKEY"
    android:value="\ 5371813415164"/>

    <!--信鸽推送静态注册-->
    <meta-data
    android:name="XGPUSH_ACCESS_ID"
    android:value="2100343759" />
    <meta-data
    android:name="XGPUSH_ACCESS_KEY"
    android:value="A7Q26I8SH7LV" />

    4.在Application中初始化XPush

    初始化XPush的方式有两种,根据业务需要选择一种方式就行了:

    • 静态注册
    /**
    * 静态注册初始化推送
    */
    private void initPush() {
    XPush.debug(BuildConfig.DEBUG);
    //静态注册,指定使用友盟推送客户端
    XPush.init(this, new UMengPushClient());
    XPush.register();
    }
    • 动态注册
    /**
    * 动态注册初始化推送
    */
    private void initPush() {
    XPush.debug(BuildConfig.DEBUG);
    //动态注册,根据平台名或者平台码动态注册推送客户端
    XPush.init(this, new IPushInitCallback() {
    @Override
    public boolean onInitPush(int platformCode, String platformName) {
    String romName = RomUtils.getRom().getRomName();
    if (romName.equals(SYS_EMUI)) {
    return platformCode == HuaweiPushClient.HUAWEI_PUSH_PLATFORM_CODE && platformName.equals(HuaweiPushClient.HUAWEI_PUSH_PLATFORM_NAME);
    } else if (romName.equals(SYS_MIUI)) {
    return platformCode == XiaoMiPushClient.MIPUSH_PLATFORM_CODE && platformName.equals(XiaoMiPushClient.MIPUSH_PLATFORM_NAME);
    } else {
    return platformCode == JPushClient.JPUSH_PLATFORM_CODE && platformName.equals(JPushClient.JPUSH_PLATFORM_NAME);
    }
    }
    });
    XPush.register();
    }

    如何使用XPush

    1、推送的注册和注销

    • 通过调用XPush.register(),即可完成推送的注册。

    • 通过调用XPush.unRegister(),即可完成推送的注销。

    • 通过调用XPush.getPushToken(),即可获取消息推送的Token(令牌)。

    • 通过调用XPush.getPlatformCode(),即可获取当前使用推送平台的码。

    2、推送的标签(tag)处理

    • 通过调用XPush.addTags(),即可添加标签(支持传入多个)。

    • 通过调用XPush.deleteTags(),即可删除标签(支持传入多个)。

    • 通过调用XPush.getTags(),即可获取当前设备所有的标签。

    需要注意的是,友盟推送和信鸽推送目前暂不支持标签的获取,华为推送不支持标签的所有操作,小米推送每次只支持一个标签的操作。

    3、推送的别名(alias)处理

    • 通过调用XPush.bindAlias(),即可绑定别名。

    • 通过调用XPush.unBindAlias(),即可解绑别名。

    • 通过调用XPush.getAlias(),即可获取当前设备所绑定的别名。

    需要注意的是,友盟推送和信鸽推送目前暂不支持别名的获取,华为推送不支持别名的所有操作。

    4、推送消息的接收

    • 通过调用XPushManager.get().register()方法,注册消息订阅MessageSubscriber,即可在任意地方接收到推送的消息。

    • 通过调用XPushManager.get().unregister()方法,即可取消消息的订阅。

    这里需要注意的是,消息订阅的回调并不一定是在主线程,因此在回调中如果进行了UI的操作,一定要确保切换至主线程。下面演示代码中使用了我的另一个开源库XAOP,只通过@MainThread注解就能自动切换至主线程,可供参考。

    /**
    * 初始化监听
    */
    @Override
    protected void initListeners() {
    XPushManager.get().register(mMessageSubscriber);
    }

    private MessageSubscriber mMessageSubscriber = new MessageSubscriber() {
    @Override
    public void onMessageReceived(CustomMessage message) {
    showMessage(String.format("收到自定义消息:%s", message));
    }

    @Override
    public void onNotification(Notification notification) {
    showMessage(String.format("收到通知:%s", notification));
    }
    };

    @MainThread
    private void showMessage(String msg) {
    tvContent.setText(msg);
    }


    @Override
    public void onDestroyView() {
    XPushManager.get().unregister(mMessageSubscriber);
    super.onDestroyView();
    }

    5、推送消息的过滤处理

    • 通过调用XPushManager.get().addFilter()方法,可增加对订阅推送消息的过滤处理。对于一些我们不想处理的消息,可以通过消息过滤器将它们筛选出来。

    • 通过调用XPushManager.get().removeFilter()方法,即可去除消息过滤器。

    /**
    * 初始化监听
    */
    @Override
    protected void initListeners() {
    XPushManager.get().addFilter(mMessageFilter);
    }

    private IMessageFilter mMessageFilter = new IMessageFilter() {
    @Override
    public boolean filter(Notification notification) {
    if (notification.getContent().contains("XPush")) {
    showMessage("通知被拦截");
    return true;
    }
    return false;
    }

    @Override
    public boolean filter(CustomMessage message) {
    if (message.getMsg().contains("XPush")) {
    showMessage("自定义消息被拦截");
    return true;
    }
    return false;
    }
    };

    @Override
    public void onDestroyView() {
    XPushManager.get().removeFilter(mMessageFilter);
    super.onDestroyView();
    }

    6、推送通知的点击处理

    对于通知的点击事件,我们可以处理得更优雅,自定义其点击后的动作,打开我们想让用户看到的页面。

    我们可以在全局消息推送的接收器IPushReceiver中的onNotificationClick回调中,增加打开指定页面的操作。

    @Override
    public void onNotificationClick(Context context, XPushMsg msg) {
    super.onNotificationClick(context, msg);
    //打开自定义的Activity
    Intent intent = IntentUtils.getIntent(context, TestActivity.class, null, true);
    intent.putExtra(KEY_PARAM_STRING, msg.getContent());
    intent.putExtra(KEY_PARAM_INT, msg.getId());
    intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
    ActivityUtils.startActivity(intent);
    }

    需要注意的是,这需要你在消息推送平台推送的通知使用的是自定义动作或者打开指定页面类型,并且传入的Intent uri 内容满足如下格式:

    • title:通知的标题

    • content:通知的内容

    • extraMsg:通知附带的拓展字段,可存放json或其他内容

    • keyValue:通知附带的键值对

    xpush://com.xuexiang.xpush/notification?title=这是一个通知&content=这是通知的内容&extraMsg=xxxxxxxxx&keyValue={"param1": "1111", "param2": "2222"}

    当然你也可以自定义传入的Intent uri 格式,具体可参考项目中的XPushNotificationClickActivityAndroidManifest.xml


    代码下载:XPush.zip

    收起阅读 »

    一行代码完成http请求!WelikeAndroid 一款引入即用的便捷开发框架

    #WelikeAndroid 是什么? WelikeAndroid 是一款引入即用的便捷开发框架,致力于为程序员打造最佳的编程体验,使用WelikeAndroid, 你会觉得写代码是一件很轻松的事情.WelikeAndroid目前包含五个大模块:异常安全隔离模...
    继续阅读 »

    #WelikeAndroid 是什么? WelikeAndroid 是一款引入即用的便捷开发框架,致力于为程序员打造最佳的编程体验,
    使用WelikeAndroid, 你会觉得写代码是一件很轻松的事情.

    WelikeAndroid目前包含五个大模块:

    • 异常安全隔离模块(实验阶段):当任何线程抛出任何异常,我们的异常隔离机制都会让UI线程继续运行下去.
    • Http模块: 一行代码完成POST、GET请求和Download,支持上传, 高度优化Disk的缓存加载机制,
      自由设置缓存大小、缓存时间(也支持永久缓存和不缓存).
    • Bitmap模块: 一行代码完成异步显示图片,无需考虑OOM问题,支持加载前对图片做自定义处理.
    • Database模块: 支持NotNull,Table,ID,Ignore等注解,Bean无需Getter和Setter,一键式部署数据库.
    • ui操纵模块: 我们为Activity基类做了完善的封装,继承基类可以让代码更加优雅.
    • :请不要认为功能相似,框架就不是原创,源码摆在眼前,何不看一看?

    使用WelikeAndroid需要以下权限:

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.INTERNET" />

    ##下文将教你如何圆润的使用WelikeAndroid:
    ###通过WelikeContext在任意处取得上下文:

    • WelikeContext.getApplication(); 就可以取得当前App的上下文
    • WelikeToast.toast("你好!"); 简单一步弹出Toast.

    ##WelikeGuard(异常安全隔离机制用法):

    • 第一步,开启异常隔离机制:
    WelikeGuard.enableGuard();
    • 第二步,注册一个全局异常监听器:

    WelikeGuard.registerUnCaughtHandler(new Thread.UncaughtExceptionHandler() {
    @Override
    public void uncaughtException(Thread thread, Throwable ex) {

    WelikeGuard.newThreadToast("出现异常了: " + ex.getMessage() );

    }
    });
    • 你也可以自定义异常:

    /**
    *
    * 自定义的异常,当异常被抛出后,会自动回调onCatchThrowable函数.
    */
    @Catch(process = "onCatchThrowable")
    public class CustomException extends IllegalAccessError {

    public static void onCatchThrowable(Thread t){
    WeLog.e(t.getName() + " 抛出了一个异常...");
    }
    }
    • 另外,继承自UncaughtThrowable的异常我们不会对其进行拦截.

    使用Welike做屏幕适配:

    Welike的ViewPorter类提供了屏幕适配的Fluent-API,我们可以通过一组流畅的API轻松做好屏幕适配.

            ViewPorter.from(button).ofScreen().divWidth(2).commit();//宽度变为屏幕的二分之一
    ViewPorter.from(button).of(viewGroup).divHeight(2).commit();//高度变为viewGroup的二分之一
    ViewPorter.from(button).div(2).commit();//宽度和高度变为屏幕的四分之一
    ViewPorter.from(button).of(this).fillWidth().fillHeight().commit();//宽度和高度铺满Activity
    ViewPorter.from(button).sameAs(imageView).commit();//button的宽度和高度和imageView一样

    WelikeHttp入门:

    • 第一步,取得WelikeHttp默认实例.
    WelikeHttp welikeHttp = WelikeHttp.getDefault();
    • 第二步,发送一个Get请求.
    HttpParams params = new HttpParams();
    params.putParams("app","qr.get",
    "data","Test");//一次性放入两对 参数 和 值

    //发送Get请求
    HttpRequest request = welikeHttp.get("http://api.k780.com:88", params, new HttpResultCallback() {
    @Override
    public void onSuccess(String content) {
    super.onSuccess(content);
    WelikeToast.toast("返回的JSON为:" + content);
    }

    @Override
    public void onFailure(HttpResponse response) {
    super.onFailure(response);
    WelikeToast.toast("JSON请求发送失败.");
    }

    @Override
    public void onCancel(HttpRequest request) {
    super.onCancel(request);
    WelikeToast.toast("请求被取消.");
    }
    });

    //取消请求,会回调onCancel()
    request.cancel();

    当然,我们为满足需求提供了多种扩展的Callback,目前我们提供以下Callback供您选择:

    • HttpCallback(响应为byte[]数组)
    • FileUploadCallback(仅在上传文件时使用)
    • HttpBitmapCallback(建议使用Bitmap模块)
    • HttpResultCallback(响应为String)
    • DownloadCallback(仅在download时使用)

    如需自定义Http模块的配置(如缓存时间),请查看HttpConfig.

    WelikeBitmap入门:

    • 第一步,取得默认的WelikeBitmap实例:

    //取得默认的WelikeBitmap实例
    WelikeBitmap welikeBitmap = WelikeBitmap.getDefault();
    • 第二步,异步加载一张图片:
    BitmapRequest request = welikeBitmap.loadBitmap(imageView,
    "http://img0.imgtn.bdimg.com/it/u=937075122,1381619862&fm=21&gp=0.jpg",
    android.R.drawable.btn_star,//加载中显示的图片
    android.R.drawable.ic_delete,//加载失败时显示的图片
    new BitmapCallback() {

    @Override
    public Bitmap onProcessBitmap(byte[] data) {
    //如果需要在加载时处理图片,可以在这里处理,
    //如果不需要处理,就返回null或者不复写这个方法.
    return null;
    }

    @Override
    public void onPreStart(String url) {
    super.onPreStart(url);
    //加载前回调
    WeLog.d("===========> onPreStart()");
    }

    @Override
    public void onCancel(String url) {
    super.onCancel(url);
    //请求取消时回调
    WeLog.d("===========> onCancel()");
    }

    @Override
    public void onLoadSuccess(String url, Bitmap bitmap) {
    super.onLoadSuccess(url, bitmap);
    //图片加载成功后回调
    WeLog.d("===========> onLoadSuccess()");
    }

    @Override
    public void onRequestHttp(HttpRequest request) {
    super.onRequestHttp(request);
    //图片需要请求http时回调
    WeLog.d("===========> onRequestHttp()");
    }

    @Override
    public void onLoadFailed(HttpResponse response, String url) {
    super.onLoadFailed(response, url);
    //请求失败时回调
    WeLog.d("===========> onLoadFailed()");
    }
    });
    • 如果需要自定义Config,请看BitmapConfig这个类.

    ##WelikeDAO入门:

    • 首先写一个Bean.

    /*表名,可有可无,默认为类名.*/
    @Table(name="USER",afterTableCreate="afterTableCreate")
    public class User{
    @ID
    public int id;//id可有可无,根据自己是否需要来加.

    /*这个注解表示name字段不能为null*/
    @NotNull
    public String name;

    public static void afterTableCreate(WelikeDao dao){
    //在当前的表被创建时回调,可以在这里做一些表的初始化工作
    }
    }
    • 然后将它写入到数据库
    WelikeDao db = WelikeDao.instance("Welike.db");
    User user = new User();
    user.name = "Lody";
    db.save(user);
    • 从数据库取出Bean

    User savedUser = db.findBeanByID(1);
    • SQL复杂条件查询
    List<User> users = db.findBeans().where("name = Lody").or("id = 1").find();
    • 更新指定ID的Bean
    User wantoUpdateUser = new User();
    wantoUpdateUser.name = "NiHao";
    db.updateDbByID(1,wantoUpdateUser);
    • 删除指ID定的Bean
    db.deleteBeanByID(1);
    • 更多实例请看DEMO和API文档.

    ##十秒钟学会WelikeActivity

    • 我们将Activity的生命周期划分如下:

    =>@initData(所有标有InitData注解的方法都最早在子线程被调用)
    =>initRootView(bundle)
    =>@JoinView(将标有此注解的View自动findViewByID和setOnClickListener)
    =>onDataLoaded(数据加载完成时回调)
    =>点击事件会回调onWidgetClick(View Widget)

    ###关于@JoinView的细节:

    • 有以下三种写法:
    @JoinView(name = "welike_btn")
    Button welikeBtn;
    @JoinView(id = R.id.welike_btn)
    Button welikeBtn;
    @JoinView(name = "welike_btn",click = false)
    Button welikeBtn;
    • clicktrue时会自动调用view的setOnClickListener方法,并在onWidgetClick回调.
    • 当需要绑定的是一个Button的时候, click属性默认为true,其它的View则默认为false.
    收起阅读 »

    Android上一个非常优雅好用的日历,全面自定义UI,自定义周起始

    CalenderViewAndroid上一个非常优雅、高度自定义、性能高效的日历控件,完美支持周视图,支持标记、自定义颜色、农历等,任意控制月视图显示、任意日期拦截条件、自定义周起始等。Canvas绘制,极速性能、占用内存低,,支持简单定制即可实现任意自定义布...
    继续阅读 »


    CalenderView

    Android上一个非常优雅、高度自定义、性能高效的日历控件,完美支持周视图,支持标记、自定义颜色、农历等,任意控制月视图显示、任意日期拦截条件、自定义周起始等。Canvas绘制,极速性能、占用内存低,,支持简单定制即可实现任意自定义布局、自定义UI,支持收缩展开、性能非常高效, 这个控件内存和效率优势相当明显,而且真正做到收缩+展开,适配多种场景,支持同时多种颜色标记日历事务,支持多点触控,你真的想不到日历还可以如此优雅!更多参考用法请移步Demo,Demo实现了一些精美的自定义效果,用法仅供参考。

    插拔式设计

    插拔式设计:好比插座一样,插上灯泡就会亮,插上风扇就会转,看用户需求什么而不是看插座有什么,只要是电器即可。此框架使用插拔式,既可以在编译时指定年月日视图,如:app:month_view="xxx.xxx.MonthView.class",也可在运行时动态更换年月日视图,如:CalendarView.setMonthViewClass(MonthView.Class),从而达到UI即插即用的效果,相当于框架不提供UI实现,让UI都由客户端实现,不至于日历UI都千篇一律,只需遵守插拔式接口即可随意定制,自由化程度非常高。

    AndroidStudio请使用3.5以上版本

    support使用版本

    implementation 'com.haibin:calendarview:3.6.8'

    Androidx使用版本

    implementation 'com.haibin:calendarview:3.6.9'
    <dependency>
    <groupId>com.haibin</groupId>
    <artifactId>calendarview</artifactId>
    <version>3.6.9</version>
    <type>pom</type>
    </dependency>

    混淆proguard-rules

    -keepclasseswithmembers class * {
    public <init>(android.content.Context);
    }

    或者针对性的使用混淆,请自行配置测试!

    -keep class your project path.MonthView {
    public <init>(android.content.Context);
    }
    -keep class your project path.WeekBar {
    public <init>(android.content.Context);
    }
    -keep class your project path.WeekView {
    public <init>(android.content.Context);
    }
    -keep class your project path.YearView {
    public <init>(android.content.Context);
    }


    特别的,请注意不要复制这三个路径,自行替换您自己的自定义路径

    app:month_view="com.haibin.calendarviewproject.simple.SimpleMonthView"
    app:week_view="com.haibin.calendarviewproject.simple.SimpleWeekView"
    app:week_bar_view="com.haibin.calendarviewproject.EnglishWeekBar"

    使用方法

     <com.haibin.calendarview.CalendarLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    app:default_status="shrink"
    app:calendar_show_mode="only_week_view"
    app:calendar_content_view_id="@+id/recyclerView">

    <com.haibin.calendarview.CalendarView
    android:id="@+id/calendarView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#fff"
    app:month_view="com.haibin.calendarviewproject.simple.SimpleCalendarCardView"
    app:week_view="com.haibin.calendarviewproject.simple.SimpleWeekView"
    app:week_bar_view="com.haibin.calendarviewproject.EnglishWeekBar"
    app:calendar_height="50dp"
    app:current_month_text_color="#333333"
    app:current_month_lunar_text_color="#CFCFCF"
    app:min_year="2004"
    app:other_month_text_color="#e1e1e1"
    app:scheme_text="假"
    app:scheme_text_color="#333"
    app:scheme_theme_color="#333"
    app:selected_text_color="#fff"
    app:selected_theme_color="#333"
    app:week_start_with="mon"
    app:week_background="#fff"
    app:month_view_show_mode="mode_only_current"
    app:week_text_color="#111" />

    <android.support.v7.widget.RecyclerView
    android:id="@+id/recyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#d4d4d4" />
    </com.haibin.calendarview.CalendarLayout>

    CalendarView attrs

    <declare-styleable name="CalendarView">

    <attr name="calendar_padding" format="dimension" /><!--日历内部左右padding-->

    <attr name="month_view" format="color" /> <!--自定义类日历月视图路径-->
    <attr name="week_view" format="string" /> <!--自定义类周视图路径-->
    <attr name="week_bar_height" format="dimension" /> <!--星期栏的高度-->
    <attr name="week_bar_view" format="color" /> <!--自定义类周栏路径,通过自定义则 week_text_color week_background xml设置无效,当仍可java api设置-->
    <attr name="week_line_margin" format="dimension" /><!--线条margin-->

    <attr name="week_line_background" format="color" /><!--线条颜色-->
    <attr name="week_background" format="color" /> <!--星期栏的背景-->
    <attr name="week_text_color" format="color" /> <!--星期栏文本颜色-->
    <attr name="week_text_size" format="dimension" /><!--星期栏文本大小-->

    <attr name="current_day_text_color" format="color" /> <!--今天的文本颜色-->
    <attr name="current_day_lunar_text_color" format="color" /><!--今天的农历文本颜色-->

           <attr name="calendar_height" format="string" /> <!--日历每项的高度,56dp-->
    <attr name="day_text_size" format="string" /> <!--天数文本大小-->
    <attr name="lunar_text_size" format="string" /> <!--农历文本大小-->

    <attr name="scheme_text" format="string" /> <!--标记文本-->
    <attr name="scheme_text_color" format="color" /> <!--标记文本颜色-->
    <attr name="scheme_month_text_color" format="color" /> <!--标记天数文本颜色-->
    <attr name="scheme_lunar_text_color" format="color" /> <!--标记农历文本颜色-->

    <attr name="scheme_theme_color" format="color" /> <!--标记的颜色-->

    <attr name="selected_theme_color" format="color" /> <!--选中颜色-->
    <attr name="selected_text_color" format="color" /> <!--选中文本颜色-->
    <attr name="selected_lunar_text_color" format="color" /> <!--选中农历文本颜色-->

    <attr name="current_month_text_color" format="color" /> <!--当前月份的字体颜色-->
    <attr name="other_month_text_color" format="color" /> <!--其它月份的字体颜色-->

    <attr name="current_month_lunar_text_color" format="color" /> <!--当前月份农历节假日颜色-->
    <attr name="other_month_lunar_text_color" format="color" /> <!--其它月份农历节假日颜色-->

    <!-- 年视图相关 -->
    <attr name="year_view_month_text_size" format="dimension" /> <!-- 年视图月份字体大小 -->
    <attr name="year_view_day_text_size" format="dimension" /> <!-- 年视图月份日期字体大小 -->
    <attr name="year_view_month_text_color" format="color" /> <!-- 年视图月份字体颜色 -->
    <attr name="year_view_day_text_color" format="color" /> <!-- 年视图日期字体颜色 -->
    <attr name="year_view_scheme_color" format="color" /> <!-- 年视图标记颜色 -->

    <attr name="min_year" format="integer" />  <!--最小年份1900-->
     <attr name="max_year" format="integer" /> <!--最大年份2099-->
    <attr name="min_year_month" format="integer" /> <!--最小年份对应月份-->
    <attr name="max_year_month" format="integer" /> <!--最大年份对应月份-->

    <!--月视图是否可滚动-->
    <attr name="month_view_scrollable" format="boolean" />
    <!--周视图是否可滚动-->
    <attr name="week_view_scrollable" format="boolean" />
    <!--年视图是否可滚动-->
    <attr name="year_view_scrollable" format="boolean" />
           
    <!--配置你喜欢的月视图显示模式模式-->
    <attr name="month_view_show_mode">
    <enum name="mode_all" value="0" /> <!--全部显示-->
    <enum name="mode_only_current" value="1" /> <!--仅显示当前月份-->
    <enum name="mode_fix" value="2" /> <!--自适应显示,不会多出一行,但是会自动填充-->
    </attr>

    <!-- 自定义周起始 -->
    <attr name="week_start_with">
    <enum name="sun" value="1" />
    <enum name="mon" value="2" />
    <enum name="sat" value="7" />
    </attr>

    <!-- 自定义选择模式 -->
    <attr name="select_mode">
    <enum name="default_mode" value="0" />
    <enum name="single_mode" value="1" />
    <enum name="range_mode" value="2" />
    </attr>

    <!-- 当 select_mode=range_mode -->
    <attr name="min_select_range" format="integer" />
    <attr name="max_select_range" format="integer" />
    </declare-styleable>

    CalendarView api


    public void setRange(int minYear, int minYearMonth, int minYearDay,
    int maxYear, int maxYearMonth, int maxYearDay) ;//置日期范围

    public int getCurDay(); //今天
    public int getCurMonth(); //当前的月份
    public int getCurYear(); //今年

    public boolean isYearSelectLayoutVisible();//年月份选择视图是否打开
    public void closeYearSelectLayout();//关闭年月视图选择布局
    public void showYearSelectLayout(final int year); //快速弹出年份选择月份

    public void setOnMonthChangeListener(OnMonthChangeListener listener);//月份改变事件

    public void setOnYearChangeListener(OnYearChangeListener listener);//年份切换事件

    public void setOnCalendarSelectListener(OnCalendarSelectListener listener)//日期选择事件

    public void setOnCalendarLongClickListener(OnCalendarLongClickListener listener);//日期长按事件

    public void setOnCalendarLongClickListener(OnCalendarLongClickListener listener, boolean preventLongPressedSelect);//日期长按事件

    public void setOnCalendarInterceptListener(OnCalendarInterceptListener listener);//日期拦截和日期有效性绘制

    public void setSchemeDate(Map<String, Calendar> mSchemeDates);//标记日期

    public void update();//动态更新

    public Calendar getSelectedCalendar(); //获取选择的日期

    /**
    * 特别的,如果你需要自定义或者使用其它选择器,可以用以下方法进行和日历联动
    */
    public void scrollToCurrent();//滚动到当前日期

    public void scrollToCurrent(boolean smoothScroll);//滚动到当前日期

    public void scrollToYear(int year);//滚动到某一年

    public void scrollToPre();//滚动到上一个月

    public void scrollToNext();//滚动到下一个月

    public void scrollToCalendar(int year, int month, int day);//滚动到指定日期

    public Calendar getMinRangeCalendar();//获得最小范围日期

    public Calendar getMaxRangeCalendar();//获得最大范围日期

    /**
    * 设置背景色
    *
    * @param monthLayoutBackground 月份卡片的背景色
    * @param weekBackground 星期栏背景色
    * @param lineBg 线的颜色
    */
    public void setBackground(int monthLayoutBackground, int weekBackground, int lineBg)

    /**
    * 设置文本颜色
    *
    * @param curMonthTextColor 当前月份字体颜色
    * @param otherMonthColor 其它月份字体颜色
    * @param lunarTextColor 农历字体颜色
    */
    public void setTextColor(int curMonthTextColor,int otherMonthColor,int lunarTextColor)

    /**
    * 设置选择的效果
    *
    * @param style 选中的style CalendarCardView.STYLE_FILL or CalendarCardView.STYLE_STROKE
    * @param selectedThemeColor 选中的标记颜色
    * @param selectedTextColor 选中的字体颜色
    */
    public void setSelectedColor(int style, int selectedThemeColor, int selectedTextColor)

    /**
    * 设置标记的色
    *
    * @param style 标记的style CalendarCardView.STYLE_FILL or CalendarCardView.STYLE_STROKE
    * @param schemeColor 标记背景色
    * @param schemeTextColor 标记字体颜色
    */
    public void setSchemeColor(int style, int schemeColor, int schemeTextColor)


    /**
    * 设置星期栏的背景和字体颜色
    *
    * @param weekBackground 背景色
    * @param weekTextColor 字体颜色
    */
    public void setWeeColor(int weekBackground, int weekTextColor)

    CalendarLayout api

    public void expand(); //展开

    public void shrink(); //收缩

    public boolean isExpand();//是否展开了

    CalendarLayout attrs


    <!-- 日历显示模式 -->
    <attr name="calendar_show_mode">
    <enum name="both_month_week_view" value="0" /><!-- 默认都有 -->
    <enum name="only_week_view" value="1" /><!-- 仅周视图 -->
    <enum name="only_month_view" value="2" /><!-- 仅月视图 -->
    </attr>

    <attr name="default_status">
    <enum name="expand" value="0" /> <!--默认展开-->
    <enum name="shrink" value="1" /><!--默认搜索-->
    </attr>

    <attr name="calendar_content_view_id" format="integer" /><!--内容布局id-->

    代码下载:CalendarView.zip

    收起阅读 »

    iOS-ijkplayer集成

    ijkplayer是bibiliB站开源的一个三方,封装好了ffmpeg,可以去面向对象去开发。苹果提供了:AVPlayer播放不了直播文件。需要自己去基于ffmpeg播放。1.搜索查找ijkplayer2.克隆ijkplayer到桌面cd Desktop/ ...
    继续阅读 »

    ijkplayer是bibiliB站开源的一个三方,封装好了ffmpeg,可以去面向对象去开发。

    苹果提供了:AVPlayer播放不了直播文件。需要自己去基于ffmpeg播放。

    1.搜索查找ijkplayer





    2.克隆ijkplayer到桌面

    cd Desktop/
    git clone https://github.com/Bilibili/ijkplayer.git ijkplayer-ios



    3.下载ffmpeg


    4.编译ffmpeg


    编译很多情况,64位、32位


    ps: 如果提示错误:

    ./libavutil/arm/asm.S:50:9: error: unknown directive
    .arch armv7-a
    ^
    make: *** [libavcodec/arm/aacpsdsp_neon.o] Error 1
    最新的 Xcode 已经弱化了对 32 位的支持, 解决方法:
    在 compile-ffmpeg.sh 中删除 armv7 , 修改如:
    FF_ALL_ARCHS_IOS8_SDK="arm64 i386 x86_64"
    再重新执行出现错误的命令: ./compile-ffmpeg.sh all

    5.打包framwork并合并

    大家会发现除了IJKMediaFramework这个目标,还有一个叫IJKMediaFrameworkWithSSL,但是不推荐使用这个,因为大部分基于ijkplayer的第三方框架都是使用的前者,你把后者导入项目还是会报找不到包的错误,就算你要支持https也推荐使用前者,然后按照上一步添加openssl即可支持

    5.1,配置释放模式如下图



    5.2,打包真机框架


    如图操作,然后按键命令+ B编译即可

    如果之前的步骤删除了compile-ffmpeg.sh中armv7,这里会报错,我们直接注释掉就好


    用Xcode9可以找到这个 ,但是用Xcode10找不到这个 我只能用Xcode注释完,在用Xcode10编译就没问题了

    5.3,打包模拟器 framework


    如图操作,然后命令+ B编译即可

    5.4,合并框架
    如果只需要真机运行或者模拟器运行,可以不用合并,直接找到对应的框架导入项目即可; 一般我们为了方便会合并框架,这样就同时支持模拟器和真机运行。
    先找到生成框架的目录:



    准备合并:

    打开终端, 先 cd 到 Products 目录下
    然后执行: lipo -create 真机framework路径 模拟器framework路径 -output 合并的文件路径

    lipo -create Release-iphoneos/IJKMediaFramework.framework/IJKMediaFramework Release-iphonesimulator/IJKMediaFramework.framework/IJKMediaFramework -output IJKMediaFramework

    合并完成:
    可以看到这里生成了一个大概两倍大小的文件, 将生成的 IJKMediaFramework 文件替换掉 真机framework 中的 IJKMediaFramework 文件,然后这个替换掉文件的 真机framework 就是我们需要的 通用的framework 了。



    6.集成 framework 到项目中

    1、导入 framework

    直接将 IJKMediaFramework.framework 拖入到工程中即可
    注意记得勾选 Copy items if needed 和 对应的 target

    2、添加下列依赖到工程


    【参考文章】:
    1、ijkplayer 的编译、打包 framework 和 https 支持
    2、armv7 armv7s arm64
    3、iOS IJKPlayer项目集成(支持RTSP)
    4、可用rtmp直播源

    链接:https://www.jianshu.com/p/9a69af13835e

    收起阅读 »

    一文速览苹果WWDC 2021:没有硬件发布的夜晚,iOS 15才是主角

    WWDC 2021在成功把M1芯片置入到了iPad Pro之后,我们最关心的另一个问题是,iPad Pro是否能有足够的软件生态来最大程度的利用好这颗高性能核心。当你带着这样的期待去收看这届的WWDC 2021之时,你会发现自己的全部期待都落了空——iPadO...
    继续阅读 »

    WWDC 2021

    在成功把M1芯片置入到了iPad Pro之后,我们最关心的另一个问题是,iPad Pro是否能有足够的软件生态来最大程度的利用好这颗高性能核心。

    当你带着这样的期待去收看这届的WWDC 2021之时,你会发现自己的全部期待都落了空——iPadOS并没有得到给力的软件生态支持,并且外界谣传的14英寸版的MacBook Pro也并没有登场。

    这次的WWDC 2021总结起来,就是三个关键词:共享、统一与隐私。

    iOS 15:更注重分享,也更注重你的「数字健康」

    视频通话变得越来越重要,苹果也为自家iOS 15加入了语音突显模式和宽频谱模式。前者可使用机器学习降低环境噪音,增强人声;后者将捕捉周围一切的声音,可以理解为没有经过通话降噪的原声。




    Share Play

    当然,比起音频增强,更多人关心的是「人像模式」——在使用FaceTime之时,iPhone不仅可以帮助你虚化掉背景,更为重要的是它居然可以帮助你进行实时美颜,当然仅限于在FaceTime通话中。

    好在现在的FaceTime已经支持网页接入了,换句话来说,就是除了苹果设备之外,Windows设备和Android手机也能够通过苹果用户分享的链接加入到FaceTime通话中了。

    一旦接受了这种设定,你就会发现苹果有多重视「与朋友/家人共享」这件事情了。这里苹果推出了功能,也是这次全系统更新的核心功能——SharePlay。它可让用户在FaceTime通话时,共享音乐、视频以及屏幕。

    有了这个功能,你就可以像使用钉钉/飞书/腾讯会议等等一系列的协同类App一样,与同事协同工作,与家人一同刷剧,与朋友一同打游戏

    分享不止于此,在苹果的官方信息应用iMessage中,现在新加入了分享Apple Music中的音乐,Apple News中的文章等等功能。





    专注模式

    为了给你的现实生活和数字生活划上一道界线,iOS 15终于加入了专注模式。这次专注模式,笔者认为是此前「睡眠模式」的延伸——如果说睡眠模式是屏蔽掉一切通知消息,那么专注模式就是可选择性的屏蔽。

    你可以设置不同的专注模式,iOS 15会帮你筛选相应的信息。比如,工作模式下,你就只能收到钉钉/微信的消息,而游戏和视频类App的推送就会被忽略掉,并且iOS 15会通过算法判断,哪一项消息更重要,并且将之置顶显示,以避免你错过重要信息。

    当然,你也可以自定义不同「专注页面」,在开启相应的专注模式之后,iOS设备就会自动显示相对应的页面。



    iOS 15新功能

    每一年的iOS系统升级,同样会伴随大量的系统应用升级,这次也不例外


    Text Live

    今年的相机和图库功能的升级方面主要是体现在,对于AI算法的利用层面上。新增的Text Live功能,它可以识别拍摄/现有图片中的文本,不仅能够转换文字,还能够进行翻译,首发支持英语、汉语、法语等七种语言。

    图库中的「回忆」功能再次升级,这次用户可以自定义回忆功能,包括音乐、动画、主题等等。系统也可根据照片的内容和风格,自动匹配合适的歌曲、节奏以及呈现的效果。

    钱包功能也得到了升级:这次它不仅能添加信用卡和公交卡,它还支持模拟酒店门卡,迪士尼公园门票,甚至是电子sfz。目前尚不清楚,它能否替代掉你的小区门禁卡。

    天气和地图应用的更新升级,则更多的体现在视觉动效的呈现上面:不同的天气会有不同的动画效果,海外部分城市的地图,支持查看海拔高度、地标景点、道路细节等。新增的公交模式,可帮助用户尽快找到附近的公交站。

    另外,值得一提的是移动端的Safari现在也支持安装浏览器拓展插件了,并且新加入了「标签组」功能——这一功能与微软推出的Edge浏览器的「集锦」功能类似。

    iOS 15还为健康应用带来了一些新功能,允许用户与医疗团队共享数据,评估跌倒风险的指标,以及趋势分析等等。此外还可以将健康数据与家庭成员共享,让关心你的人第一时间了解你的身体状态。



    AirPods升级

    顺便一提,AirPods(主要是AirPods Pro和AirPods Max)也得到了小幅度的功能升级,比如新增了对话增强模式,利用计算音频和波束成形麦克风,AirPods 可实现更清晰的对话;新增了通知播报功能,AirPods 可自动阅读具有时效性的通知内容;以及新增了和AirTag类似的防丢功能。

    简单来说,如果AirPods遗失在外,其会自动发出蓝牙信号,路过的iPhone识别到上传到iCloud,直达用户的「查找app」。至于有些音乐发烧友期待的更高清的码率更新并没有到来,Apple Music也只是新增了Dolby Atmos音效。



    OS 15支持的设备

    令人意外的是,iOS 15支持的机型与 iOS 14基本一致。iPhone 6s、第一代iPhone SE也可升级。开发者预览版现在已经开始推送更新了,至于公测版则在7月份,也就是下个月开始推送更新,正式版会在秋季发布会之后更新。

    iPadOS:你要的Mac级应用并未出现

    iPadOS大部分的新功能与iOS 15一样,不过苹果还是为大屏幕新增了一些独有功能,比如说更大尺寸的小组件——现在小组件终于能够与App图标混排了。





    iPadOS升级一览

    借助这一功能,你可以在iPad上打造出更个性化的页面,比如游戏页面,追剧页面等等,同时iPadOS也终于加入了和iOS一样的App资源库的功能。

    同时,iPadOS终于更新分屏操作的逻辑:新增了「多任务控制板」和「App组合架」的操作逻辑。通过多任务控制板,你不仅可以双开应用,甚至可以「三开」——就像之前华为的「智慧分屏」功能一样,拥有第三个悬浮的浏览页面。


    多任务新特性一览

    同时,你还能够将不同的分屏页面「放」在App组合架上,便于你在多个不同的分屏应用之间快速切换。你可以通过拖动应用程序来创建一个新的分屏视图,比传统的多任务还要方便。而这些操作,也都可以借助iPad妙控键盘用快捷键实现。

    苹果也对iPadOS上的备忘录功能进行了升级:你可以在任意应用的角落里,通过手指/Apple Pencil滑动呼出备忘录小窗,快速记录包括手写笔记、连接、Safari 高亮内容、便签等等任何一闪而过的灵感。

    快速笔记也是支持多设备同步的,例如你在 Safari 中对某段文字做了备注,当你再次浏览时,便会出现快速笔记的缩略图,将你带回之前浏览过的内容。

    iPadOS上Swift Playgrounds的更新,可能是这次唯一称得上是与生产力挂钩的升级了。Swift Playgrounds是苹果推出的可视化的编程操作App。这次的更新允许用户直接在Swift Playgrounds中开发App,并且进行调试甚至是直接上架到App Store进行销售。

    尽管与Mac采用同一种M1芯片的iPad Pro已经推出,但iPadOS的升级更多的是「适配更大屏幕的iOS」的逻辑,而非是想要将iPadOS打造成更强生产力,能让它取代掉Mac。这还是让笔者有些失望。

    watchOS&macOS:小幅度升级,跨设备交互功能亮眼

    今年的watchOS更新还是从两个层面上:一是新增了「照片表盘」功能,你可以将任意图片设置成表盘,这张图片是具备景深效果的,你可以通过表冠来调节虚化效果。




    watchOS新特性一览

    二是在健康应用层面上,watchOS为「呼吸」功能新增了更漂亮的动画,让「睡眠」除了能记录你的睡眠时长之外,还能记录下你的睡眠呼吸频率,从而分析出你的睡眠质量。最后订阅服务,Apple Fitness+则是增加了两种热门体能训练——太极和普拉提。

    新的macOS被命名为Monterey,源自加州的蒙特雷市。新功能与iOS保持一致,但拥有足以改变多设备交互方式的Universal Control功能。


    Universal Control

    简单来说,通过Universal Control,你能在靠近的不同苹果设备之间共享一套键鼠,并且能够在不同设备之间快速共享文件。比如,你可以通过MacBook上的键盘和触控板,修改iPad上的图片/文稿等等,并且可以直接将文字/图片拖动到当前Mac编辑的文稿/剪辑的视频时间线之中。



    macOS新特性一览

    此次更新中,最让笔者兴奋的功能是,AirPlay to Mac——你终于能够把移动端的内容通过AirPlay的方式直接投屏到Mac上,通过Mac的大屏和更棒的扬声器,享受更舒适的视听体验了。

    最后是iOS上的快捷指令功能被移植到了macOS之上,你终于能够通过自动化的指令,在Mac电脑上名正言顺地「偷懒」了。

    隐私:从在世到离世,苹果都在为你的隐私考虑

    隐私保护一直是苹果极为重视的方面,这次的多系统更新也一样:这次苹果为原生的「邮件」App新增了隐私保护功能,它不仅能够隐藏你的IP地址,还能隐藏你打开邮件的动作,以确保送信者无法得知你何时,甚至是否打开了邮件。

    此前,苹果为iOS设备增加了更多的设备权限管理功能,这次则是新增了App隐私报告。你可以透过它很直观地看到哪些应用使用了相关隐私权限的次数和时间等数据。

    Siri增加了语音识别功能。默认设置下,发给Siri的对话将在设备本地处理,不上传至云端。这也意味着Siri可以在离线状态下完成更多的指令,比如打开某个应用,设置提醒/闹钟等等。

    为了保护用户的隐私,原有的iCloud业务也升级成了iCloud+:在浏览网页之时,用户可以通过iCloud建立一条加密的链接,实现更安全的访问。

    iCloud+还可以给用户生成随机的电子邮件地址,并转发到用户的收件箱。所以在网上填写表格或新用户注册时,不必输入个人真实的电子邮箱。

    此外,苹果还新增了「数字遗产计划」:用户可以自行定义遗产联系人,万一用户不幸离世之后,透过这项功能,该联系人可以申请访问离世用户的iCloud数据。

    iCloud升级成了iCloud+,但其订阅价格并未改变:50GB存储空间每月付费6元,并且支持一个HomeKit安全摄像头(监控视频无限存储空间);200GB存储空间每月付费21元,支持最多五个HomeKit安全摄像头;2TB存储空间每月付费68元,支持无上限个数的HomeKit安全摄像头。

    写在最后:它既是连接数字生活的纽带,也是分割现实生活的界线

    每一届的WWDC都会带来苹果设备的系统级更新,而每一次的更新都会让苹果生态系统内的设备关系更加紧密,尤其是随着M1芯片的推出以及在不同平特设备上的应用(Mac和iPad Pro)。

    这种密切的联系不仅仅是多设备之间的协同,更是不同设备之间同一种交互逻辑,同一种应用功能和界面。从这次的SharePlay功能和FaceTime跨平台支持的功能来看,苹果不仅想要牢牢绑定现有生态内的用户,还想要拉入其他平台的用户进来,体验苹果生态带来的统一性。

    当然,这些更新中最有意思的,还是苹果对于科技与生活的理解:在你使用苹果设备之时,它不仅在意用户的数字隐私,也在意用户的身体健康。

    在iOS 15公测版推送更新之后,笔者也将会第一时间给各位读者带来最新的体验。


    作者/唐植潇

    本文首发钛媒体APP

    原地址:https://baijiahao.baidu.com/s?id=1701962186485997583&wfr=spider&for=pc



    收起阅读 »

    iOS年度盛会 --- iOS 15新增8大更新

    各位果粉们早上好,相信不少果粉和小编一样,熬夜看完了苹果这次WWDC开发者大会。看完发布会的第一感受--就这?这可能是近几年来最枯燥无味的一场开发者大会了,要不是以为有“one more thing...”,估计小编看到一半就睡着了。开个玩笑,虽然今年的WWD...
    继续阅读 »
    各位果粉们早上好,相信不少果粉和小编一样,熬夜看完了苹果这次WWDC开发者大会。看完发布会的第一感受--就这?这可能是近几年来最枯燥无味的一场开发者大会了,要不是以为有“one more thing...”,估计小编看到一半就睡着了。
    开个玩笑,虽然今年的WWDC大会可能没那么精彩,但苹果还是用了近两小时的时间向我们介绍了iOS 15、iPadOS 15、 watchOS 8、tvOS 15以及MacOS Monterey系统,没有one more thing...,没有新硬件发布!


    1、FaceTime视频通话升级
    言归正传,接下来就给大家分享一下iOS 15都加入了哪些新功能,首先介绍的是iOS 15系统升级了FaceTime视频通话,包括加入了空间音频的支持、人声增强、人像模式背景虚化、以及第三方设备支持通过链接打开FaceTime等等。

    当你使用FaceTime进行通话时,还能给一起视频的小伙伴们分享视频、歌曲。让用户可以在视频的同时,还能一起同步播放视频、歌曲。支持共享的视频包括迪士尼、NBA、HBO以及Tik Tok等知名视频平台。

    2、新增「与你共享」功能
    为了方便用户共享更多内容,苹果在iOS 15中加入了“与你共享”新功能,首批支持的的App包括照片、音乐、Safari浏览器、播客等等。

    3、通知中心升级
    iOS 15对通知中心也进行了升级,通知中心图标将更大,让用户能更轻松识别通知来源。不仅如此,iOS 15中还引入了“通知摘要”功能,用户可以自己设置某一个App的通知时间,且通知仅显示重要通知内容,过滤掉无关信息,以保证用户不会错过这条提示。

    4、「专注模式」来了
    另外,iOS 15还加入了「专注模式」,包括勿扰模式、工作模式、个人模式以及睡眠模式。每个状态可以设置不同的显示通知,并可与其他设备同步。

    5、照片新增「实况文本」
    接下来就是照片的升级,iOS 15中为照片加入了「实况文本」功能,在这个功能的帮助下,iPhone相机可自动扫描并识别文字,用户可以长按进行选择、复制与粘贴。毫不夸张的说,这个可能是本次iOS 15更新最实用的功能之一了~

    得益于神经网络学习的加持,「实况文本」可识别iPhone中所有照片的文字,支持包括英语和中文等7种文字识别,用户可直接搜索照片中的文字找到这张照片。

    6、iPhone门禁卡也来了
    苹果在iOS 15中加入了钱包钥匙功能,这些钥匙包括公司徽章、酒店房间钥匙和家庭智能锁钥匙。你的iPhone可以解锁你的家、你的车库、你的酒店房间,甚至你的工作场所。如此看来,iPhone当门禁卡的功能来了。

    7、天气App升级
    天气App在iOS 15进行了升级,不仅可以显示更多关于天气的信息,新的天气App会根据天气情况的变化而改变。

    8、地图更智能、更详细
    全新升级的地图不仅显示信息更丰富,同时还将为驾驶员提供更多详细道路信息。地图还会自动跟踪用户的出行路线,如果用户迷路,可扫描附近建筑,通过增强现实给用户提供正确路线。假如用户乘坐公交出行,还能提醒用户什么时间下车。

    以上就是iOS 15系统的主要更新内容了,小编已经第一时间更新了iOS 15系统。从使用半天的感受来看,目前iOS 15并无明显影响使用到Bug,仅部分新功能还未完全汉化,首个iOS 15测试版还是很流畅的,想要尝鲜iOS 15的果粉可以放心升级。
    转自:果粉技巧公众号
    收起阅读 »

    鸿蒙开源第三方组件——SlidingMenu_ohos侧滑菜单组件

    前言 基于安卓平台的SlidingMenu侧滑菜单组件(github.com/jfeinstein1… 实现了鸿蒙化迁移和重构,代码已经开源到(gitee.com/isrc\_ohos/… 欢迎各位下载使用并提出宝贵意见! 背景 SlidingMen...
    继续阅读 »

    前言


    基于安卓平台的SlidingMenu侧滑菜单组件(github.com/jfeinstein1…


    实现了鸿蒙化迁移和重构,代码已经开源到(gitee.com/isrc\_ohos/…


    欢迎各位下载使用并提出宝贵意见!


    背景


    SlidingMenu_ohos提供了一个侧滑菜单的导航框架,使菜单可以隐藏在手机屏幕的左侧、右侧或左右两侧。当用户使用时,通过左滑或者右滑的方式调出,既节省了主屏幕的空间,也方便用户操作,在很多主流APP中都有广泛的应用。


    效果展示


    由于菜单从左右两侧调出的显示效果相似,此处仅以菜单从左侧调出为例进行效果展示。


    组件未启用时,应用显示主页面。单指触摸屏幕左侧并逐渐向右滑动,菜单页面逐渐显示,主页面逐渐隐藏。向右滑动的距离超过某个阈值时,菜单页面全部显示,效果如图1所示。


    鸿蒙开源第三方组件——SlidingMenu_ohos侧滑菜单组件


    图1 菜单展示和隐藏效果图


    Sample解析


    Sample部分的内容较为简单,主要包含两个部分。一是创建SlidingMenu_ohos组件的对象,可根据用户的实际需求,调用Library的接口,对组件的具体属性进行设置。二是将设置好的组件添加到Ability中。下面将详细介绍组件的使用方法。


    1、导入SlidingMenu类


    import com.jeremyfeinstein.slidingmenu.lib.SlidingMenu;

    2、设置Ability的布局


    此布局用作为主页面的布局,在组件隐藏的时候显示。


    DirectionalLayout directionalLayout = 
    (DirectionalLayout)LayoutScatter.getInstance(this).parse(ResourceTable.Layout_activity_main,null,false);setUIContent(directionalLayout);

    3、实例化组件的对象


    SlidingMenu slidingMenu = null;
    try {
    //初始化SlidingMenu实例
    slidingMenu = new SlidingMenu(this);
    } catch (IOException e) {
    e.printStackTrace();
    } catch (NotExistException e) {
    e.printStackTrace();
    }

    4、设置组件属性


    此步骤可以根据具体需求,设置组件的位置、触发范围、布局、最大宽度等属性。


    //设置菜单放置位置
    slidingMenu.setMode(SlidingMenu.LEFT);
    //设置组件的触发范围
    slidingMenu.setTouchScale(100);
    //设置组件的布局
    slidingMenu.setMenu(ResourceTable.Layout_layout_left_menu);
    //设置菜单最大宽度
    slidingMenu.setMenuWidth(800);

    5、关联Ability


    attachToAbility()方法是Library提供的重要方法,用于将菜单组件关联到Ability。其参数SLIDING_WINDOW和SLIDING_CONTENT是菜单的不同模式,SLIDING_WINDOW模式下的菜单包含Title / ActionBar部分,菜单需在整个手机页面上显示,如图2所示;SLIDING_CONTENT模式下的菜单不包括包含Title / ActionBar部分,菜单可以在手机页面的局部范围内显示,如图3所示。


    try {
    //关联Ability,获取页面展示根节点
    slidingMenu.attachToAbility(directionalLayout,this, SlidingMenu.SLIDING_WINDOW);
    } catch (NotExistException e) {
    e.printStackTrace();
    } catch (WrongTypeException e) {
    e.printStackTrace();
    } catch (IOException e) {
    e.printStackTrace();
    }

    鸿蒙开源第三方组件——SlidingMenu_ohos侧滑菜单组件


    图2 SLIDING_WINDOW展示效果图


    鸿蒙开源第三方组件——SlidingMenu_ohos侧滑菜单组件


    图3 SLIDING_CONTENT展示效果图


    Library解析


    Library的工程结构如下图所示,CustomViewAbove表示主页面,CustomViewBehind表示菜单页面,SlidingMenu主要用于控制主页面位于菜单页面的上方,还可以设置菜单的宽度、触发范围、显示模式等属性。为了方便解释,以下均以手指从左侧触摸屏幕并向右滑动为例进行讲解,菜单均采用SLIDING_WINDOW的显示模式。


    鸿蒙开源第三方组件——SlidingMenu_ohos侧滑菜单组件


    图4 Library的工程结构


    1、CustomViewAbove主页面


    CustomViewAbove需要监听触摸、移动、抬起和取消等Touch事件,并记录手指滑动的距离和速度。


    (1)对Touch事件的处理


    Touch事件决定了菜单的显示、移动和隐藏。例如:在菜单的触发范围内,手指向右滑动(POINT_MOVE)时,菜单会跟随滑动到手指所在位置。手指抬起(PRIMARY_POINT_UP)或者取消滑动(CANCEL)时,会依据手指滑动的距离和速度决定菜单页面的下一状态是全部隐藏还是全部显示。


     switch (action) {
    //按下
    case TouchEvent.PRIMARY_POINT_DOWN:
    .....
    mInitialMotionX=mLastMotionX=ev.getPointerPosition(mActivePointerId).getX();
    break;
    //滑动
    case TouchEvent.POINT_MOVE:
    ......
    //菜单滑动到此时手指所在位置(x)
    left_scrollto(x);
    break;
    //抬起
    case TouchEvent.PRIMARY_POINT_UP:
    ......
    //获得菜单的下一状态(全屏显示或者全部隐藏)
    int nextPage = determineTargetPage(pageOffset, initialVelocity,totalDelta);
    //设置菜单的下一状态
    setCurrentItemInternal(nextPage,initialVelocity);
    ......
    endDrag();
    break;
    //取消
    case TouchEvent.CANCEL:
    ......
    //根据菜单当前状态mCurItem设置菜单下一状态
    setCurrentItemInternal(mCurItem);
    //结束拖动
    endDrag();
    break;
    }

    (2)对滑动的距离和速度的处理


    手指抬起时,滑动的速度和距离分别大于最小滑动速度和最小移动距离,判定此时的操作为快速拖动,菜单立即弹出并全部显示,如图5所示。


    private int determineTargetPage(float pageOffset, int velocity, int deltaX) {
    //获得当前菜单状态,0:左侧菜单正在展示,1:菜单隐藏,2:右侧菜单正在展示
    int targetPage = getCurrentItem();
    //针对快速拖动的判断
    if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
    if (velocity > 0 && deltaX > 0) {
    targetPage -= 1;
    } else if (velocity < 0 && deltaX < 0){
    targetPage += 1;
    }
    }
    }

    鸿蒙开源第三方组件——SlidingMenu_ohos侧滑菜单组件


    图5 快速拖动效果图


    当手指抬起并且不满足快速拖动标准时,需要根据滑动距离判断菜单的隐藏或显示。若菜单已展开的部分超过自身宽度的1/2,菜单立即弹出全部显示,,效果图如图1所示;若不足自身宽度的1/2,则立即弹回全部隐藏,效果图如图6所示。


    //获得当前菜单状态,0:左侧菜单正在展示,1:菜单隐藏,2:右侧菜单正在展示
    switch (mCurItem){
    case 0:
    targetPage=1-Math.round(pageOffset);
    break;
    case 1:
    //菜单隐藏时,首先要判断此时菜单的放置状态是左侧还是右侧
    if(current_state == SlidingMenu.LEFT){
    targetPage = Math.round(1-pageOffset);
    }
    if(current_state == SlidingMenu.RIGHT){
    targetPage = Math.round(1+pageOffset);
    }
    break;
    case 2:
    targetPage = Math.round(1+pageOffset);
    break;
    }

    鸿蒙开源第三方组件——SlidingMenu_ohos侧滑菜单组件


    图6 缓慢拖动效果图


    (3)菜单显示和隐藏的实现


    主页面的左侧边线与手指的位置绑定,当手指向右滑动时,主页面也会随手指向右滑动,在这个过程中菜单页面渐渐展示出来,实现菜单页面随手指滑动慢慢展开的视觉效果。


    void setCurrentItemInternal(int item,int velocity) {
    //获得菜单的目标状态
    item = mViewBehind.getMenuPage(item);
    mCurItem = item;
    final int destX = getDestScrollX(mCurItem);
    /*菜单放置状态为左侧,通过设置主页面的位置实现菜单的弹出展示或弹回隐藏
    1.destX=0,主页面左侧边线与屏幕左侧边线对齐,菜单被全部遮挡,实现菜单弹回隐藏
    2.destX=MenuWidth,主页面左侧边线向右移动与菜单总宽度相等的距离,实现菜单弹出展示*/
    if (mViewBehind.getMode() == SlidingMenu.LEFT) {
    mContent.setLeft(destX);
    mViewBehind.scrollBehindTo(destX);
    }
    ......
    }

    // 菜单放置在左侧时的菜单滑动操作
    public void left_scrollto(float x) {
    //当menu的展示宽度大于最大宽度时仅展示最大宽度
    if(x>getMenuWidth()){
    x=getMenuWidth();
    }
    //主页面(主页面左侧边线)和菜单(菜单右侧边线)分别移动到指定位置X
    mContent.setLeft((int)x);
    mViewBehind.scrollBehindTo((int)x);
    }

    2、CustomViewBehind 菜单页面


    CustomViewBehind为菜单页面,逻辑相比于主页面简单许多。主要负责根据主页面中的Touch事件改变自身状态值,同时向外暴露接口,用于设置或者获取菜单页面的最大宽度、自身状态等属性。


    // 设置菜单最大宽度
    public void setMenuWidth(int menuWidth) {
    this.menuWidth = menuWidth;
    }

    // 获得菜单最大宽度
    public int getMenuWidth() {
    return menuWidth;
    }

    3. SlidingMenu


    分别实例化CustomViewAbove和CustomViewBehind的对象,并按照主页面在上菜单页面在下的顺序分别添加到SlidingMenu的容器中。


    //添加菜单子控件
    addComponent(mViewBehind, behindParams);
    //添加主页面子控件
    addComponent(mViewAbove, aboveParams);

    项目贡献人


    徐泽鑫 郑森文 朱伟 陈美汝 王佳思 张馨心


    作者:朱伟ISRC

    收起阅读 »

    面试官问我:如何使用LeakCanary排查Android中的内存泄露,看我如何用漫画装逼!

    1)在项目的build.gradle文件添加: debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5' releaseCompile 'com.squareup.leakc...
    继续阅读 »



    在这里插入图片描述



    在这里插入图片描述



    在这里插入图片描述



    在这里插入图片描述



    在这里插入图片描述



    在这里插入图片描述



    在这里插入图片描述

    1)在项目的build.gradle文件添加:


        debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
    releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
    testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'

    可以看到,debugCompile跟releaseCompile 引入的是不同的包, 在 debug 版本上,集成 LeakCanary 库,并执行内存泄漏监测,而在 release 版本上,集成一个无操作的 wrapper ,这样对程序性能就不会有影响。


    2)在Application类添加:


    public class LCApplication extends Application {
    @Override
    public void onCreate() {
    super.onCreate();
    if (LeakCanary.isInAnalyzerProcess(this)) {
    // This process is dedicated to LeakCanary for heap analysis.
    // You should not init your app in this process.
    return;
    }
    LeakCanary.install(this);
    // Normal app init code...
    }
    }

    LeakCanary.install() 会返回一个预定义的 RefWatcher,同时也会启用一个 ActivityRefWatcher,用于自动监控调用 Activity.onDestroy() 之后泄露的 activity。


    如果是简单的检测activity是否存在内存泄漏,上面两个步骤就可以了,是不是很简单。 那么当某个activity存在内存泄漏的时候,会有什么提示呢?LeakCanary会自动展示一个通知栏,点开提示框你会看到引起内存溢出的引用堆栈信息。




    这里写图片描述



    在这里插入图片描述

    具体使用代码


    1)Application 相关代码:


    public class LCApplication extends Application {
    @Override
    public void onCreate() {
    super.onCreate();
    if (LeakCanary.isInAnalyzerProcess(this)) {
    // This process is dedicated to LeakCanary for heap analysis.
    // You should not init your app in this process.
    return;
    }
    LeakCanary.install(this);
    // Normal app init code...
    }

    }

    2)泄漏的activity类代码:


    public class MainActivity extends Activity {

    private Button next;

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

    next = (Button) findViewById(R.id.next);
    next.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    Intent intent = new Intent(MainActivity.this, SecondActivity.class);
    startActivity(intent);
    finish();
    }
    });
    new Thread(new Runnable() {
    @Override
    public void run() {
    while (true) {
    System.out.println("=================");
    }
    }
    }).start();
    }
    }

    当点击next跳到第二个界面后,LeakCanary会自动展示一个通知栏,点开提示框你会看到引起内存溢出的引用堆栈信息,如上图所示,这样你就很容易定位到原来是线程引用住当前activity,导致activity无法释放。



    在这里插入图片描述



    在这里插入图片描述



    在这里插入图片描述

    上面提到,LeakCanary.install() 会返回一个预定义的 RefWatcher,同时也会启用一个 ActivityRefWatcher,用于自动监控调用 Activity.onDestroy() 之后泄露的 activity。现在很多app都使用到了fragment,那fragment如何检测呢。


    1)Application 中获取到refWatcher对象。


    public class LCApplication extends Application {

    public static RefWatcher refWatcher;

    @Override
    public void onCreate() {
    super.onCreate();
    if (LeakCanary.isInAnalyzerProcess(this)) {
    // This process is dedicated to LeakCanary for heap analysis.
    // You should not init your app in this process.
    return;
    }
    refWatcher = LeakCanary.install(this);
    // Normal app init code...
    }
    }

    2)使用 RefWatcher 监控 Fragment:


    public abstract class BaseFragment extends Fragment {
    @Override public void onDestroy() {
    super.onDestroy();
    RefWatcher refWatcher = LCApplication.refWatcher;
    refWatcher.watch(this);
    }
    }

    这样则像监听activity一样监听fragment。其实这种方式一样适用于任何对象,比如图片,自定义类等等,非常方便。




    在这里插入图片描述



    在这里插入图片描述



    在这里插入图片描述



    在这里插入图片描述



    在这里插入图片描述



    在这里插入图片描述



    在这里插入图片描述



    在这里插入图片描述

    LeakCanary.install(this)源码如下所示:


    public static RefWatcher install(Application application) {
    return ((AndroidRefWatcherBuilder)refWatcher(application).listenerServiceClass(DisplayLeakService.class).excludedRefs(AndroidExcludedRefs.createAppDefaults().build())).buildAndInstall();
    }

    listenerServiceClass(DisplayLeakService.class):用于分析内存泄漏结果信息,然后发送通知给用户。 excludedRefs(AndroidExcludedRefs.createAppDefaults().build()):设置需要忽略的对象,比如某些系统漏洞不需要统计。 buildAndInstall():真正检测内存泄漏的方法,下面将展开分析该方法。


    public RefWatcher buildAndInstall() {
    RefWatcher refWatcher = this.build();
    if(refWatcher != RefWatcher.DISABLED) {
    LeakCanary.enableDisplayLeakActivity(this.context);
    ActivityRefWatcher.installOnIcsPlus((Application)this.context, refWatcher);
    }

    return refWatcher;
    }

    可以看到,上面方法主要做了三件事情: 1.实例化RefWatcher对象,该对象主要作用是检测是否有对象未被回收导致内存泄漏; 2.设置APP图标可见; 3.检测内存



    在这里插入图片描述



    在这里插入图片描述

    RefWatcher的使用后面讲,这边主要看第二件事情的处理过程,及enableDisplayLeakActivity方法的源码


    public static void enableDisplayLeakActivity(Context context) {
    LeakCanaryInternals.setEnabled(context, DisplayLeakActivity.class, true);
    }

    public static void setEnabled(Context context, final Class<?> componentClass, final boolean enabled) {
    final Context appContext = context.getApplicationContext();
    executeOnFileIoThread(new Runnable() {
    public void run() {
    LeakCanaryInternals.setEnabledBlocking(appContext, componentClass, enabled);
    }
    });
    }

    public static void setEnabledBlocking(Context appContext, Class<?> componentClass, boolean enabled) {
    ComponentName component = new ComponentName(appContext, componentClass);
    PackageManager packageManager = appContext.getPackageManager();
    int newState = enabled?1:2;
    packageManager.setComponentEnabledSetting(component, newState, 1);
    }

    可见,最后调用packageManager.setComponentEnabledSetting()方法,实现应用图标的隐藏和显示。



    在这里插入图片描述



    在这里插入图片描述

    接下来,进入真正的内存检查的方法installOnIcsPlus()


    public static void installOnIcsPlus(Application application, RefWatcher refWatcher) {
    if(VERSION.SDK_INT >= 14) {
    ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
    activityRefWatcher.watchActivities();
    }
    }

    该方法实例化出ActivityRefWatcher 对象,该对象用来监听activity的生命周期,具体实现如下所示:


    public void watchActivities() {
    this.stopWatchingActivities();
    this.application.registerActivityLifecycleCallbacks(this.lifecycleCallbacks);
    }

    private final ActivityLifecycleCallbacks lifecycleCallbacks = new ActivityLifecycleCallbacks() {
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
    }

    public void onActivityStarted(Activity activity) {
    }

    public void onActivityResumed(Activity activity) {
    }

    public void onActivityPaused(Activity activity) {
    }

    public void onActivityStopped(Activity activity) {
    }

    public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
    }

    public void onActivityDestroyed(Activity activity) {
    ActivityRefWatcher.this.onActivityDestroyed(activity);
    }
    };



    在这里插入图片描述



    在这里插入图片描述

    调用了registerActivityLifecycleCallbacks方法后,当Activity执行onDestroy方法后,会触发ActivityLifecycleCallbacks 的onActivityDestroyed方法,在当前方法中,调用refWatcher的watch方法,前面已经讲过RefWatcher对象主要作用是检测是否有对象未被回收导致内存泄漏。下面继续看refWatcher的watch方法源码:


    public void watch(Object watchedReference) {
    this.watch(watchedReference, "");
    }

    public void watch(Object watchedReference, String referenceName) {
    if(this != DISABLED) {
    Preconditions.checkNotNull(watchedReference, "watchedReference");
    Preconditions.checkNotNull(referenceName, "referenceName");
    long watchStartNanoTime = System.nanoTime();
    String key = UUID.randomUUID().toString();
    this.retainedKeys.add(key);
    KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, this.queue);
    this.ensureGoneAsync(watchStartNanoTime, reference);
    }
    }

    可以看到,上面方法主要做了三件事情: 1.生成一个随机数key存放在retainedKeys集合中,用来判断对象是否被回收; 2.把当前Activity放到KeyedWeakReference(WeakReference的子类)中; 3.通过查找ReferenceQueue,看该Acitivity是否存在,存在则证明可以被正常回收,不存在则证明可能存在内存泄漏。 前两件事很简单,这边主要看第三件事情的处理过程,及ensureGoneAsync方法的源码:


    private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
    this.watchExecutor.execute(new Retryable() {
    public Result run() {
    return RefWatcher.this.ensureGone(reference, watchStartNanoTime);
    }
    });
    }

    Result ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    long watchDurationMs = TimeUnit.NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
    this.removeWeaklyReachableReferences();
    if(this.debuggerControl.isDebuggerAttached()) {
    return Result.RETRY;
    } else if(this.gone(reference)) {
    return Result.DONE;
    } else {
    this.gcTrigger.runGc();
    this.removeWeaklyReachableReferences();
    if(!this.gone(reference)) {
    long startDumpHeap = System.nanoTime();
    long gcDurationMs = TimeUnit.NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
    File heapDumpFile = this.heapDumper.dumpHeap();
    if(heapDumpFile == HeapDumper.RETRY_LATER) {
    return Result.RETRY;
    }

    long heapDumpDurationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
    this.heapdumpListener.analyze(new HeapDump(heapDumpFile, reference.key, reference.name, this.excludedRefs, watchDurationMs, gcDurationMs, heapDumpDurationMs));
    }

    return Result.DONE;
    }
    }

    该方法中首先执行removeWeaklyReachableReferences(),从ReferenceQueue队列中查询是否存在该弱引用对象,如果不为空,则说明已经被系统回收了,则将对应的随机数key从retainedKeys集合中删除。


     private void removeWeaklyReachableReferences() {
    KeyedWeakReference ref;
    while((ref = (KeyedWeakReference)this.queue.poll()) != null) {
    this.retainedKeys.remove(ref.key);
    }
    }

    然后通过判断retainedKeys集合中是否存在对应的key判断该对象是否被回收。


    private boolean gone(KeyedWeakReference reference) {
    return !this.retainedKeys.contains(reference.key);
    }

    如果没有被系统回收,则手动调用gcTrigger.runGc();后再调用removeWeaklyReachableReferences方法判断该对象是否被回收。


    GcTrigger DEFAULT = new GcTrigger() {
    public void runGc() {
    Runtime.getRuntime().gc();
    this.enqueueReferences();
    System.runFinalization();
    }

    private void enqueueReferences() {
    try {
    Thread.sleep(100L);
    } catch (InterruptedException var2) {
    throw new AssertionError();
    }
    }
    };

    第三行代码为手动触发GC,紧接着线程睡100毫秒,给系统回收的时间,随后通过System.runFinalization()手动调用已经失去引用对象的finalize方法。 通过手动GC该对象还不能被回收的话,则存在内存泄漏,调用heapDumper.dumpHeap()生成.hprof文件目录,并通过heapdumpListener回调到analyze()方法,后面关于dump文件的分析这边就不介绍了,感兴趣的可以自行去看。



    在这里插入图片描述



    在这里插入图片描述



    在这里插入图片描述








    作者:天才少年_
    链接:https://juejin.cn/post/6844904165265670157
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。




    收起阅读 »

    想做图表?Android优秀图表库MPAndroidChart

    嗨,你终于来啦 ~ 等你好久啦~ 喜欢的小伙伴欢迎关注,我会定期分享Android知识点及解析,还会不断更新的BATJ面试专题,欢迎大家前来探讨交流,如有好的文章也欢迎投稿。 前言 在项目当中很多时候要对数据进行分析就要用到图表,在gitHub上有很多优秀的...
    继续阅读 »

    嗨,你终于来啦 ~ 等你好久啦~ 喜欢的小伙伴欢迎关注,我会定期分享Android知识点及解析,还会不断更新的BATJ面试专题,欢迎大家前来探讨交流,如有好的文章也欢迎投稿。

    前言


    在项目当中很多时候要对数据进行分析就要用到图表,在gitHub上有很多优秀的图表开源库,今天给大家分享的就是MPAndroidChart中的柱状图。简单介绍一下MPAndroidChart:他可以实现图表的拖动,3D,局部查看,数据动态展示等功能。


    官方源码地址:github.com/PhilJay/MPA…


    废话就不多说啦,先给看大家看看效果图哟



























    操作步骤


    第一步:需要将依赖的库添加到你的项目中



    implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0-alpha'

    implementation 'com.google.android.material:material:1.0.0'



    第二步:xml中


       <com.github.mikephil.charting.charts.BarChart
    android:id="@+id/chart1"
    android:layout_width="match_parent"
    android:layout_height="300dp"
    />

    第三步:ValueFormatter.java


      /**
    * Class to format all values before they are drawn as labels.
    */

    public abstract class ValueFormatter implements IAxisValueFormatter, IValueFormatter {

    /**
    * <b>DO NOT USE</b>, only for backwards compatibility and will be removed in future versions.
    *
    * @param value the value to be formatted
    * @param axis the axis the value belongs to
    * @return formatted string label
    */

    @Override
    @Deprecated
    public String getFormattedValue(float value, AxisBase axis) {
    return getFormattedValue(value);
    }

    /**
    * <b>DO NOT USE</b>, only for backwards compatibility and will be removed in future versions.
    * @param value the value to be formatted
    * @param entry the entry the value belongs to - in e.g. BarChart, this is of class BarEntry
    * @param dataSetIndex the index of the DataSet the entry in focus belongs to
    * @param viewPortHandler provides information about the current chart state (scale, translation, ...)
    * @return formatted string label
    */

    @Override
    @Deprecated
    public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) {
    return getFormattedValue(value);
    }

    /**
    * Called when drawing any label, used to change numbers into formatted strings.
    *
    * @param value float to be formatted
    * @return formatted string label
    */

    public String getFormattedValue(float value) {
    return String.valueOf(value);
    }

    /**
    * Used to draw axis labels, calls {@link #getFormattedValue(float)} by default.
    *
    * @param value float to be formatted
    * @param axis axis being labeled
    * @return formatted string label
    */

    public String getAxisLabel(float value, AxisBase axis) {
    return getFormattedValue(value);
    }

    /**
    * Used to draw bar labels, calls {@link #getFormattedValue(float)} by default.
    *
    * @param barEntry bar being labeled
    * @return formatted string label
    */

    public String getBarLabel(BarEntry barEntry) {
    return getFormattedValue(barEntry.getY());
    }

    /**
    * Used to draw stacked bar labels, calls {@link #getFormattedValue(float)} by default.
    *
    * @param value current value to be formatted
    * @param stackedEntry stacked entry being labeled, contains all Y values
    * @return formatted string label
    */

    public String getBarStackedLabel(float value, BarEntry stackedEntry) {
    return getFormattedValue(value);
    }

    /**
    * Used to draw line and scatter labels, calls {@link #getFormattedValue(float)} by default.
    *
    * @param entry point being labeled, contains X value
    * @return formatted string label
    */

    public String getPointLabel(Entry entry) {
    return getFormattedValue(entry.getY());
    }

    /**
    * Used to draw pie value labels, calls {@link #getFormattedValue(float)} by default.
    *
    * @param value float to be formatted, may have been converted to percentage
    * @param pieEntry slice being labeled, contains original, non-percentage Y value
    * @return formatted string label
    */

    public String getPieLabel(float value, PieEntry pieEntry) {
    return getFormattedValue(value);
    }

    /**
    * Used to draw radar value labels, calls {@link #getFormattedValue(float)} by default.
    *
    * @param radarEntry entry being labeled
    * @return formatted string label
    */

    public String getRadarLabel(RadarEntry radarEntry) {
    return getFormattedValue(radarEntry.getY());
    }

    /**
    * Used to draw bubble size labels, calls {@link #getFormattedValue(float)} by default.
    *
    * @param bubbleEntry bubble being labeled, also contains X and Y values
    * @return formatted string label
    */

    public String getBubbleLabel(BubbleEntry bubbleEntry) {
    return getFormattedValue(bubbleEntry.getSize());
    }

    /**
    * Used to draw high labels, calls {@link #getFormattedValue(float)} by default.
    *
    * @param candleEntry candlestick being labeled
    * @return formatted string label
    */

    public String getCandleLabel(CandleEntry candleEntry) {
    return getFormattedValue(candleEntry.getHigh());
    }

    }

    第四步:MyValueFormatter


        public class MyValueFormatter extends ValueFormatter{
    private final DecimalFormat mFormat;
    private String suffix;

    public MyValueFormatter(String suffix) {
    mFormat = new DecimalFormat("0000");
    this.suffix = suffix;
    }

    @Override
    public String getFormattedValue(float value) {
    return mFormat.format(value) + suffix;
    }

    @Override
    public String getAxisLabel(float value, AxisBase axis) {
    if (axis instanceof XAxis) {
    return mFormat.format(value);
    } else if (value > 0) {
    return mFormat.format(value) + suffix;
    } else {
    return mFormat.format(value);
    }
    }
    }

    第五步:MainAcyivity


      package detongs.hbqianze.him.linechart;
    import android.os.Bundle;
    import android.util.Log;
    import android.view.WindowManager;
    import android.widget.TextView;

    import androidx.appcompat.app.AppCompatActivity;

    import com.github.mikephil.charting.charts.BarChart;
    import com.github.mikephil.charting.components.XAxis;
    import com.github.mikephil.charting.components.YAxis;
    import com.github.mikephil.charting.data.BarData;
    import com.github.mikephil.charting.data.BarDataSet;
    import com.github.mikephil.charting.data.BarEntry;
    import com.github.mikephil.charting.interfaces.datasets.IBarDataSet;
    import com.github.mikephil.charting.interfaces.datasets.IDataSet;
    import com.github.mikephil.charting.utils.ColorTemplate;

    import java.util.ArrayList;

    import detongs.hbqianze.him.linechart.chart.MyValueFormatter;
    import detongs.hbqianze.him.linechart.chart.ValueFormatter;

    public class MainActivity extends AppCompatActivity {



    private BarChart chart;
    private TextView te_cache;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
    WindowManager.LayoutParams.FLAG_FULLSCREEN);
    setContentView(R.layout.activity_main);


    chart = findViewById(R.id.chart1);
    te_cache = findViewById(R.id.te_cache);


    chart.getDescription().setEnabled(false);

    //设置最大值条目,超出之后不会有值
    chart.setMaxVisibleValueCount(60);

    //分别在x轴和y轴上进行缩放
    chart.setPinchZoom(true);
    //设置剩余统计图的阴影
    chart.setDrawBarShadow(false);
    //设置网格布局
    chart.setDrawGridBackground(true);
    //通过自定义一个x轴标签来实现2,015 有分割符符bug
    ValueFormatter custom = new MyValueFormatter(" ");
    //获取x轴线
    XAxis xAxis = chart.getXAxis();

    //设置x轴的显示位置
    xAxis.setPosition(XAxis.XAxisPosition.BOTTOM);
    //设置网格布局
    xAxis.setDrawGridLines(true);
    //图表将避免第一个和最后一个标签条目被减掉在图表或屏幕的边缘
    xAxis.setAvoidFirstLastClipping(false);
    //绘制标签 指x轴上的对应数值 默认true
    xAxis.setDrawLabels(true);
    xAxis.setValueFormatter(custom);
    //缩放后x 轴数据重叠问题
    xAxis.setGranularityEnabled(true);
    //获取右边y标签
    YAxis axisRight = chart.getAxisRight();
    axisRight.setStartAtZero(true);
    //获取左边y轴的标签
    YAxis axisLeft = chart.getAxisLeft();
    //设置Y轴数值 从零开始
    axisLeft.setStartAtZero(true);

    chart.getAxisLeft().setDrawGridLines(false);
    //设置动画时间
    chart.animateXY(600,600);

    chart.getLegend().setEnabled(true);

    getData();
    //设置柱形统计图上的值
    chart.getData().setValueTextSize(10);
    for (IDataSet set : chart.getData().getDataSets()){
    set.setDrawValues(!set.isDrawValuesEnabled());
    }



    }



    public void getData(){
    ArrayList<BarEntry> values = new ArrayList<>();
    Float aFloat = Float.valueOf("2015");
    Log.v("xue","aFloat+++++"+aFloat);
    BarEntry barEntry = new BarEntry(aFloat,Float.valueOf("100"));
    BarEntry barEntry1 = new BarEntry(Float.valueOf("2016"),Float.valueOf("210"));
    BarEntry barEntry2 = new BarEntry(Float.valueOf("2017"),Float.valueOf("300"));
    BarEntry barEntry3 = new BarEntry(Float.valueOf("2018"),Float.valueOf("450"));
    BarEntry barEntry4 = new BarEntry(Float.valueOf("2019"),Float.valueOf("300"));
    BarEntry barEntry5 = new BarEntry(Float.valueOf("2020"),Float.valueOf("650"));
    BarEntry barEntry6 = new BarEntry(Float.valueOf("2021"),Float.valueOf("740"));
    values.add(barEntry);
    values.add(barEntry1);
    values.add(barEntry2);
    values.add(barEntry3);
    values.add(barEntry4);
    values.add(barEntry5);
    values.add(barEntry6);
    BarDataSet set1;

    if (chart.getData() != null &&
    chart.getData().getDataSetCount() > 0) {
    set1 = (BarDataSet) chart.getData().getDataSetByIndex(0);
    set1.setValues(values);
    chart.getData().notifyDataChanged();
    chart.notifyDataSetChanged();
    } else {
    set1 = new BarDataSet(values, "点折水");
    set1.setColors(ColorTemplate.VORDIPLOM_COLORS);
    set1.setDrawValues(false);

    ArrayList<IBarDataSet> dataSets = new ArrayList<>();
    dataSets.add(set1);

    BarData data = new BarData(dataSets);
    chart.setData(data);

    chart.setFitBars(true);
    }
    //绘制图表
    chart.invalidate();

    }

    }




    github地址:https://github.com/PhilJay/MPAndroidChart

    下载地址:MPAndroidChart-master.zip

    收起阅读 »

    性能优化你会吗 --- iOS开发中常见的性能优化技巧

    性能问题的主要原因是什么,原因有相同的,也有不同的,但归根到底,不外乎内存使用、代码效率、合适的策略逻辑、代码质量、安装包体积这一类问题。但从用户体验的角度去思考,当我们置身处地得把自己当做用户去玩一款应用时候,那么都会在意什么呢?假如正在玩一款手游,首先一定...
    继续阅读 »

    性能问题的主要原因是什么,原因有相同的,也有不同的,但归根到底,不外乎内存使用、代码效率、合适的策略逻辑、代码质量、安装包体积这一类问题。

    但从用户体验的角度去思考,当我们置身处地得把自己当做用户去玩一款应用时候,那么都会在意什么呢?假如正在玩一款手游,首先一定不希望玩着玩着突然闪退,然后就是不希望卡顿,其次就是耗电和耗流量不希望太严重,最后就是安装包希望能小一点。简单归类如下:

    快:使用时避免出现卡顿,响应速度快,减少用户等待的时间,满足用户期望。
    稳:不要在用户使用过程中崩溃和无响应。
    省:节省流量和耗电,减少用户使用成本,避免使用时导致手机发烫。
    小:安装包小可以降低用户的安装成本。

    一、快

    应用启动慢,使用时经常卡顿,是非常影响用户体验的,应该尽量避免出现。卡顿的场景有很多,按场景可以分为4类:UI 绘制、应用启动、页面跳转、事件响应。引起卡顿的原因很多,但不管怎么样的原因和场景,最终都是通过设备屏幕上显示来达到用户,归根到底就是显示有问题,

    根据iOS 系统显示原理可以看到,影响绘制的根本原因有以下两个方面:

    1.绘制任务太重,绘制一帧内容耗时太长。
    2.主线程太忙,根据系统传递过来的 VSYNC 信号来时还没准备好数据导致丢帧。

    绘制耗时太长,有一些工具可以帮助我们定位问题。主线程太忙则需要注意了,主线程关键职责是处理用户交互,在屏幕上绘制像素,并进行加载显示相关的数据,所以特别需要避免任何主线程的事情,这样应用程序才能保持对用户操作的即时响应。总结起来,主线程主要做以下几个方面工作:

    1.UI 生命周期控制
    2.系统事件处理
    3.消息处理
    4.界面布局
    5.界面绘制
    6.界面刷新

    除此之外,应该尽量避免将其他处理放在主线程中,特别复杂的数据计算和网络请求等。

    二、稳

    应用的稳定性定义很宽泛,影响稳定性的原因很多,比如内存使用不合理、代码异常场景考虑不周全、代码逻辑不合理等,都会对应用的稳定性造成影响。其中最常见的两个场景是:Crash 和 ANR,这两个错误将会使得程序无法使用,比较常用的解决方式如下:

    1.提高代码质量。比如开发期间的代码审核,看些代码设计逻辑,业务合理性等。
    2.代码静态扫描工具。常见工具有Clang Static Analyzer、OCLint、Infer等等。
    3.Crash监控。把一些崩溃的信息,异常信息及时地记录下来,以便后续分析解决。
    4.Crash上传机制。在Crash后,尽量先保存日志到本地,然后等下一次网络正常时再上传日志信息。

    三、省

    在移动设备中,电池的重要性不言而喻,没有电什么都干不成。对于操作系统和设备开发商来说,耗电优化一致没有停止,去追求更长的待机时间,而对于一款应用来说,并不是可以忽略电量使用问题,特别是那些被归为“电池杀手”的应用,最终的结果是被卸载。因此,应用开发者在实现需求的同时,需要尽量减少电量的消耗。

    1.CPU

    不论用户是否正在直接使用, CPU 都是应用所使用的主要硬件, 在后台操作和处理推送通知时, 应用仍然会消耗 CPU 资源

    应用计算的越多,消耗的电量越多.在完成相同的基本操作时, 老一代的设备会消耗更多的电量, 计算量的消耗取决于不同的因素

    2.网络

    智能的网络访问管理可以让应用响应的更快,并有助于延长电池寿命.在无法访问网络时,应该推迟后续的网络请求, 直到网络连接恢复为止. 此外,应避免在没有连接 WiFi 的情况下进行高宽带消耗的操作.比如视频流, 众所周知,蜂窝无线系统(LTE,4G,3G等)对电量的消耗远远大于 WiFi信号,根源在于 LTE 设备基于多输入,多输出技术,使用多个并发信号以维护两端的 LTE 链接,类似的,所有的蜂窝数据链接都会定期扫描以寻找更强的信号. 因此:我们需要

    1)在进行任何网络操作之前,先检查合适的网络连接是否可用
    2)持续监视网络的可用性,并在链接状态发生变化时给与适当的反馈
    3).定位管理器和** GPS**

    我们都知道定位服务是很耗电的,使用 GPS 计算坐标需要确定两点信息:

    1)时间锁每个 GPS 卫星每毫秒广播唯一一个1023位随机数, 因而数据传播速率是1.024Mbit/s GPS 的接收芯片必须正确的与卫星的时间锁槽对齐
    2)频率锁 GPS 接收器必须计算由接收器与卫星的相对运动导致的多普勒偏移带来的信号误差

    计算坐标会不断的使用 CPU 和 GPS 的硬件资源,因此他们会迅速的消耗电池电量, 那么怎么减少呢?

    1)关闭无关紧要的特性

    判断何时需要跟踪位置的变化, 在需要跟踪的时候调用 startUpdatingLocation方法,无须跟踪时调用stopUpdatingLocation方法.

    当应用在后台运行或用户没有与别人聊天时,也应该关闭位置跟踪,也就说说,浏览媒体库,查看朋友列表或调整应用设置时, 都应该关闭位置跟踪

    2)只在必要时使用网络

    为了提高电量的使用效率, IOS 总是尽可能地保持无线网络关闭.当应用需要建立网络连接时,IOS 会利用这个机会向后台应用分享网络会话,以便一些低优先级能够被处理, 如推送通知,收取电子邮件等

    关键在于每当用户建立网络连接时,网络硬件都会在连接完成后多维持几秒的活动时间.每次集中的网络通信都会消耗大量的电量

    要想减轻这个问题带来的危害,你的软件需要有所保留的的使用网络.应该定期集中短暂的使用网络,而不是持续的保持着活动的数据流.只有这样,网络硬件才有机会关闭

    4.屏幕

    屏幕非常耗电, 屏幕越大就越耗电.当然,如果你的应用在前台运行且与用户进行交互,则势必会使用屏幕并消耗电量

    这里有一些方案可以优化屏幕的使用:

    1)动画优化

    当应用在前台时, 使用动画,一旦应用进入了后台,则立即暂停动画.通常来说,你可以通过监听 UIApplicationWillResignActiveNotification或UIApplicationDIdEnterBackgroundNotification的通知事件来暂停或停止动画,也可以通过监听UIApplicationDidBecomeActiveNotification的通知事件来恢复动画

    2)视频优化


    视频播放期间,最好保持屏幕常量.可以使用UIApplication对象的idleTimerDisabled属性来实现这个目的.一旦设置了 YES, 他会阻止屏幕休眠,从而实现常亮.

    与动画类似,你可以通过相应应用的通知来释放和获取锁

    用户总是随身携带者手机,所以编写省电的代码就格外重要, 毕竟手机的移动电源并不是随处可见, 在无法降低任务复杂性时, 提供一个对电池电量保持敏感的方案并在适当的时机提示用户, 会让用户体验良好。

    四、小

    应用安装包大小对应用使用没有影响,但应用的安装包越大,用户下载的门槛越高,特别是在移动网络情况下,用户在下载应用时,对安装包大小的要求更高,因此,减小安装包大小可以让更多用户愿意下载和体验产品。

    当然,瘦身和减负虽好,但需要注意瘦身对于项目可维护性的影响,建议根据自身的项目进行技巧的选取。

    App安装包是由资源和可执行文件两部分组成,安装包瘦身从以下三部分优化。

    资源优化:
    1.删除无用的资源
    2.删除重复的资源
    3.无损压缩图片
    4.不常用资源换为下载

    编译优化:
    1.去除debug符号
    2.开启编译优化
    3.避免编译多个架构

    可执行文件优化:
    1.去除无用代码
    2.统计库占用,去除无用库
    3.混淆类/方法名
    4.减少冗余字符串
    5.ARC->MRC (一般不到特殊情况不建议这么做,会提高维护成本)

    缩减iOS安装包大小是很多中大型APP都要做的事,一般首先会对资源文件下手,压缩图片/音频,去除不必要的资源。这些资源优化做完后,我们还可以尝试对可执行文件进行瘦身,项目越大,可执行文件占用的体积越大,又因为AppStore会对可执行文件加密,导致可执行文件的压缩率低,压缩后可执行文件占整个APP安装包的体积比例大约有80%~90%,还是挺值得优化的。

    下面是一些常见的优化方案:
    TableViewCell 复用

    在cellForRowAtIndexPath:回调的时候只创建实例,快速返回cell,不绑定数据。在willDisplayCell: forRowAtIndexPath:的时候绑定数据(赋值)。

    高度缓存

    在tableView滑动时,会不断调用heightForRowAtIndexPath:,当cell高度需要自适应时,每次回调都要计算高度,会导致 UI 卡顿。为了避免重复无意义的计算,需要缓存高度。

    怎么缓存?

    字典,NSCache。

    UITableView-FDTemplateLayoutCell

    [if !supportLineBreakNewLine]

    [endif]

    视图层级优化

    不要动态创建视图

    在内存可控的前提下,缓存subview。

    善用hidden。

    [if !supportLineBreakNewLine]

    [endif]

    减少视图层级

    减少subviews个数,用layer绘制元素。

    少用clearColor,maskToBounds,阴影效果等。

    [if !supportLineBreakNewLine]

    [endif]

    减少多余的绘制操作

    图片

    不要用JPEG的图片,应当使用PNG图片。

    子线程预解码(Decode),主线程直接渲染。因为当image没有Decode,直接赋值给imageView会进行一个Decode操作。

    优化图片大小,尽量不要动态缩放(contentMode)。

    尽可能将多张图片合成为一张进行显示。

    [if !supportLineBreakNewLine]

    [endif]

    减少透明view

    使用透明view会引起blending,在iOS的图形处理中,blending主要指的是混合像素颜色的计算。最直观的例子就是,我们把两个图层叠加在一起,如果第一个图层的透明的,则最终像素的颜色计算需要将第二个图层也考虑进来。这一过程即为Blending。

    会导致blending的原因:

    UIView的alpha<1。

    UIImageView的image含有alpha channel(即使UIImageView的alpha是1,但只要image含有透明通道,则仍会导致blending)。

    [if !supportLineBreakNewLine]

    [endif]

    为什么blending会导致性能的损失?

    原因是很直观的,如果一个图层是不透明的,则系统直接显示该图层的颜色即可。而如果图层是透明的,则会引起更多的计算,因为需要把另一个的图层也包括进来,进行混合后的颜色计算。

    opaque设置为YES,减少性能消耗,因为GPU将不会做任何合成,而是简单从这个层拷贝。

    [if !supportLineBreakNewLine]

    [endif]

    减少离屏渲染

    离屏渲染指的是在图像在绘制到当前屏幕前,需要先进行一次渲染,之后才绘制到当前屏幕。

    OpenGL中,GPU屏幕渲染有以下两种方式:

    On-Screen

    Rendering即当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。

    Off-Screen

    Rendering即离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。

    [if !supportLineBreakNewLine]

    [endif]

    小结

    性能优化不是更新一两个版本就可以解决的,是持续性的需求,持续集成迭代反馈。在实际的项目中,在项目刚开始的时候,由于人力和项目完成时间限制,性能优化的优先级比较低,等进入项目投入使用阶段,就需要把优先级提高,但在项目初期,在设计架构方案时,性能优化的点也需要提早考虑进去,这就体现出一个程序员的技术功底了。

    什么时候开始有性能优化的需求,往往都是从发现问题开始,然后分析问题原因及背景,进而寻找最优解决方案,最终解决问题,这也是日常工作中常会用到的处理方式。

    链接:https://www.jianshu.com/p/965932858d95

    收起阅读 »