APP

APP

融云im小程序集成初体验

IM即时通讯大神庵 发表了文章 • 0 个评论 • 61 次浏览 • 2021-04-15 15:15 • 来自相关话题

最近领导让做一个小程序端的聊天功能,基于多方考虑,最终选择了融云的im来实现,那就先写个demo体验下吧。首先呢,当然是查看官方文档,文档地址如下: https://docs.rongcloud.cn/v4/views/im/noui/guide/p... ...查看全部

最近领导让做一个小程序端的聊天功能,基于多方考虑,最终选择了融云的im来实现,那就先写个demo体验下吧。

微信截图_20210415151418.png

首先呢,当然是查看官方文档,文档地址如下: https://docs.rongcloud.cn/v4/views/im/noui/guide/private/setting/include/mini.html

需要注意要在管理后台的小程序服务下将小程序的开关打开,否则会报错,相关的demo也可从后端下载到。

拿到sdk了,我们就可以上手集成了,嘻嘻……

由于是demo,暂时先把appkey和token配置写死了,放在了config.js中,appkey和token可以从管理后台获取到。

其次呢,我们需要把im引入,可以整一个公用的js文件,在此引入im sdk,初始化一个全局的im对象(在此呢需要注意im对象一定是全局的,不能赋值给小程序里面的data值,之前我就犯了错,赋值给了data,导致页面只要一调用setData方法就一直报Converting string to Json的错误,大家可以在集成过程中避雷呢)。

引入代码如下:

const RongIMLib = require("./RongIMLib-3.0.7-dev.js");
const config = require("../public/config");
const imInstance = RongIMLib.init({ appkey: config.appkey});
export {
  RongIMLib,
  config,
  imInstance
}

接着我们创建一个主页面,在这个页面中我们可以建立融云的连接以及获取会话列表。

connect() { 
    if (this.data.isConnect) {
        this.data.im.disconnect();
    }
    imInstance.connect({ token: config.token }).then(user => {
        this.isConnect = true;
        this.getConversationList();
        console.log('链接成功, 链接用户 id 为: ', user);
    }).catch(error => {
    console.log('链接失败: ', error);
 });
},

注意事项:在此需要注意安全域名问题,要不会报错,导致连接不上融云的服务。

1. 开发环境的话,可以在右上角的详情中的本地设置中将不校验合法域名的地方打上对勾。

2. 生产环境的话,可以参考如下地址设置: https://docs.rongcloud.cn/v4/views/im/noui/guide/private/setting/include/mini.html

这样的话就可以连接成功了,哈哈,第一步总算是跨出去了。

接下来我们可以在连接成功后获取会话列表,代码如下:

imInstance.Conversation.getList().then(list => {
  this.setData({
    conversationList: list
  })
}).catch(error => {
  console.log('获取会话列表失败: ', error);
});

点击详情跳转到聊天页面,在此需要注意如果要获取历史记录的话是要开通im商业版-单群聊云存储的。开通后按照api使用说明获取就可以了,代码如下:

getHistory(conversation) {
    const option = {
        timestrap: +new Date(),
        count: 10
        };
        conversation.getMessages(option).then((result) => { 
            var hasMore = result.hasMore; // 是否还有历史消息可以获取
            this.setData({
              messageList: result.list
            })
        });      
}

接下来发送消息等其他接口,正常调用就可以了,和web端sdk基本是一致的,参考官方文档即可,整个集成过程花了两三天吧,还算顺利。有不足之处还望大家多多指教,多多交流……

融云官网地址:https://www.rongcloud.cn/

融云开发文档地址:https://docs.rongcloud.cn/v4/


集成融云即时通讯碰到的一些问题

IM即时通讯赵炳东 发表了文章 • 0 个评论 • 45 次浏览 • 2021-04-15 15:15 • 来自相关话题

前言公司产品需要新增即时通讯的模块,经过调研后使用融云的即时通讯 SDK 由于以前没有做过相关聊天类的项目,在开发的时候碰到了很多坑,下面会将碰到的问题和解决方案记录下来以做备份和学习交流融云官网:https://www.rongcloud.cn/ ... ...查看全部

前言

公司产品需要新增即时通讯的模块,经过调研后使用融云的即时通讯 SDK 由于以前没有做过相关聊天类的项目,在开发的时候碰到了很多坑,下面会将碰到的问题和解决方案记录下来以做备份和学习交流

融云官网:https://www.rongcloud.cn/ 开发文档:https://docs.rongcloud.cn/v4/

问题列表

1、如何获取历史消息

解决方案: 因为 Web 端没有本地存储,不提供本地获取方法,只能从远端获取 使用获取历史消息方法需要在开发者后台开启 IM 商用版 - 单群聊云存储 服务,服务开通后 30 分钟左右生效 每次最多只能获取 20 条历史消息,通过改变参数 timestrap 来获取其他时间段的历史消息

2、删除会话后还有会话

解决方案: 1、删除会话. 正常来说只要没有收发消息, 会话就不会再生成了 2、如果您有清除 localStorage 的操作, 则 SDK 内部会重新拉取离线消息. 而会话列表是根据收发消息生成的. 则会再次产生会话. 所以建议您检查下是否有清除 localStorage 的操作. 如果有, 建议您只清除自己业务相关的 key 3、如果您换端登录, 则会再次收离线消息, 如果希望此时依然不显示删除的消息. 可以在您删除消息后发一条自定义消息比如: (1). 删除会话 A (2). 发送自定义消息, 标识删除了会话 A (3). 换端登录, 重复收之前已收到过的消息 (4). 收消息时收到了步骤 (2) 中发送的删除标识消息, 根据此自定义消息, 再次调用删除会话方法删除会话

3、怎样才能获取到加入群组之前的聊天记录

解决方案: 加入群组,融云默认是获取不到之前的历史消息,如果想获取到之前的历史消息,需要在开发者后台 -- 免费基础功能 中开通 “新用户获取加入群组前历史消息” 服务,服务开通后 30 分钟左右生效

4、消息中的接收方和发送方 ID 为什么是相同的

解决方案: 如果在接收方来查看消息的话,接收方 id 和 发送方 id 是相同的。 消息体中 targetId 表示会话 id (接收方),senderUserId 表示发送方 id ,messageDirection 表示消息方向,发送: 1,接收: 2 如果 A 给 B 发送一条消息,那么在 B 的角度看的话 targetId 为 A,senderUserId 也是 A,只是 messageDirection 为 2 表示接收 在 A 的角度看的话, targetId 为 B,senderUserId 是 A,只是 messageDirection 为 1 表示发送

5、删除历史消息时,参数与文档描述一致,但是结果却返回 33007 表示:未开通历史消息服务,但是开发者后台早已经开通了

解决方案: 因传入了比融云服务器时间还要大的时间, 所以服务返回了此错误码. 如果要保证删除成功, 建议传入会话最后一条消息的 sentTime (发送时间)

6、接收 emoji 显示不出来

解决方案: 1、Web SDK 接收消息后,消息体内的原生 Emoji 字符会被解码为对应 Unicode 码,需调用转化方法才能正确显示 2、不同浏览器, 不同设备, 展示的原生 Emoji 表情都不同 3、如需多端展示 Emoji 一致, 需使用 emojiToHTML 转化为 HTML 后再展示(此方法为以图片形式展示) 4、emojiToHTML 和 symbolToHTML 仅支持默认的 128 个 emoji. 展示更多, 需自行扩展 详情参考:https://docs.rongcloud.cn/v4/views/im/noui/guide/private/msgmanage/msgsend/web.html#emoji

暂时先记录这些,后续还有的话会继续添加进去,也欢迎大家进行补充,一起学习一下,笔芯 ♥️ ♥️ ♥️ !!!


实现类似微信视频聊天的功能

WebRTC赵炳东 发表了文章 • 0 个评论 • 50 次浏览 • 2021-04-15 15:15 • 来自相关话题

前言今年因为疫情的影响,很多线下的项目或者活动都放在了线上,所以集成音视频等相关的话题一直比较火热。 作为一个小码农对微信音视频通话的功能还是比较好奇的,所以空闲的时候就想着自己能不能也实现一个,给朋友一个链接地址然后就可以愉快的视频通话了(小小的满足一下虚荣... ...查看全部

前言

今年因为疫情的影响,很多线下的项目或者活动都放在了线上,所以集成音视频等相关的话题一直比较火热。 作为一个小码农对微信音视频通话的功能还是比较好奇的,所以空闲的时候就想着自己能不能也实现一个,给朋友一个链接地址然后就可以愉快的视频通话了(小小的满足一下虚荣心)

微信截图_20210415151315.png

对于集成音视频纯小白来说,前期准备工作的确不好整,因为对这块没什么概念,对后面需要做什么列不出一个清晰的步骤出来(蓝廋香菇~~~),幸运的是,在网上找相关知识时意外发现了一家做音视频 SDK 的(融云https://www.rongcloud.cn/

为什么推荐他们家呢,主要是他们的开发文档比较适用于像我这样的小白,跟着他们的快速文档无脑实现音视频通话。点这个地址,开启你的音视频之旅吧:https://docs.rongcloud.cn/v4/views/rtc/call/noui/quick/web.html

特别意外的是融云还提供了 Web 端在线集成体验示例,体验完后还能拿到 demo ,附上跳转连接:https://docs.rongcloud.cn/v4/views/rtc/call/noui/demo/web.html

音视频通话介绍 音视频通话 SDK 含呼叫流程,可以实现一对一呼叫、多人呼叫等音视频通话场景,音视频通话 SDK 区别于音视频会议 SDK,音视频通话(呼叫)SDK 不需要双方约定房间号,呼叫流程自动处理房间号,适用于类似微信音视频通话等必须含有呼叫流程的应用场景,由两部分组成 信令通知 和 音视频传输

信令通知:基于 IMLib SDK,例如 A 给 B 发起通话,A 优先会通过 IM 给 B 发送一个通话请求,B 同意后再进入音视频房间进行音视频通信 音视频传输:基于 RongRTCLib SDK,例如 A 给 B 发起通话,信令传输完成后,会适用融云提供的音视频能力进行通信

但是在集成的时候,有几点是需要吐槽的:

1、我给其他人发起通话的时候,直接就触发挂断了返回 "SummaryMessage" 类型消息,咋又看不懂在文档找了半天都没发现 SummaryMessage 的详细描述,只知道是挂断电话返回的消息,最后提交了工单才了解挂断的原因。

Web 端挂断原因 HungupMessage 消息中 reason 字段及 SummaryMessage 消息中 status 字段都为挂断原因

状态码说明状态码说明
1己方取消已发出的通话请求11对方取消已发出的通话请求
2己方拒绝收到的通话请求12对方拒绝收到的通话请求
3己方挂断13对方挂断
4己方忙碌14对方忙碌
5己方未接听15对方未接听
6己方不支持当前引擎16对方不支持当前引擎
7己方网络错误17对方网络错误
8其他设备已处理18CallLib 不可用

原来是对方忙碌导致的接听失败!!!

2、前段时间还能正常通话,升级完 chrome 后就报错了(一脸懵逼 ),报错信息如下: Failed to execute 'setLocalDescription' on 'RTCPeerConnection': Failed to parse SessionDescription. m=video 9 UDP/TLS/RTP/SAVPF 98 99 96 97 100 101 127 Invalid value:

提供单询问融云的技术人员,才知道是由于 chrome 86 升级了 webRTC 的规范,SDK 在 3.2.6 版本做了相关适配。如果 SDK 使用的不是 3.2.6 版本。需要升级至 3.2.6 版本


融云 IM SDK 集成 —- 刷新会话界面和会话列表界面

IM即时通讯大兴 发表了文章 • 0 个评论 • 678 次浏览 • 2020-12-02 16:28 • 来自相关话题

最近集成融云 IMkit 发现, 融云 IMkit 提供的 ConversationListFragment 和 ConversationFragment 都没有提供刷新方法.我们有个需求是, 使用 Message 的 extra 修改值后, 但是界面没有进行... ...查看全部

最近集成融云 IMkit 发现, 融云 IMkit 提供的 ConversationListFragment 和 ConversationFragment 都没有提供刷新方法.

微信截图_20201202162201.png

我们有个需求是, 使用 Message 的 extra 修改值后, 但是界面没有进行刷新, 需要自己调用代码进行刷新, 但是, 融云没有提供对应的刷新界面的方法, 所以只能看代码.

刷新 ListView 是需要获取 Adapter , 然后使用 Adapter 进行刷新, 但是我们无法从 ConversationListFragment 中直接获取, 那怎么办呢?

我们知道, ListView 中有 getAdapter 的方法. 那我们能不能先获取 ListView 控件呢? 答案是可以的. 我们通过 findById 方法即可获取 ListView. 通过查找, 我们可以知道, 融云会话列表的 listView 的 id 为 R.id.rc_list.

所以代码如下.

ListView mList = (ListView)findViewById(R.id.rc_list);
HeaderViewListAdapter adapter = (HeaderViewListAdapter)mList.getAdapter();
ConversationListAdapter wrappedAdapter = (ConversationListAdapter)adapter.getWrappedAdapter();
if (adapter != null) {
    wrappedAdapter.notifyDataSetChanged();
}


Android 端如何添加自定义表情

IM即时通讯大兴 发表了文章 • 0 个评论 • 567 次浏览 • 2020-12-02 16:28 • 来自相关话题

实现步骤1.新建 RongEmoticonTab 类继承 IEmoticonTab 。public class RongEmoticonTab implements IEmoticonTab {  &... ...查看全部

微信截图_20201202162412.png

实现步骤

1.新建 RongEmoticonTab 类继承 IEmoticonTab 。

public class RongEmoticonTab implements IEmoticonTab {
    public RongEmoticonTab() {
    }
    @Override
    public Drawable obtainTabDrawable(final Context context) {
        return context.getResources().getDrawable(R.drawable.u1f603);
    }
    @Override
    public View obtainTabPager(Context context) {
        return view;
    }
    @Override
    public void onTableSelected(int i) {
    }
}

2.在 obtainTabPager 中添加您想要展示在表情面板上的 view 。

 @Override
    public View obtainTabPager(Context context) {
        View view = LayoutInflater.from(context).inflate(R.layout.view_emoji, null);
        RecyclerView rv = view.findViewById(R.id.recycler_view);
        //LinearLayoutManager是用来做列表布局,也就是单列的列表
        GridLayoutManager mLayoutManager = new GridLayoutManager(context, 5, OrientationHelper.VERTICAL, false);
        rv.setLayoutManager(mLayoutManager);
        //谷歌提供了一个默认的item删除添加的动画
        rv.setItemAnimator(new DefaultItemAnimator());
        rv.setHasFixedSize(true);
        //模拟列表数据
        ArrayList newsList = new ArrayList<>();
        TypedArray array = context.getResources().obtainTypedArray(context.getResources().getIdentifier("rc_emoji_res", "array", context.getPackageName()));
        int i = -1;
        while (++i < array.length()) {
            newsList.add(array.getResourceId(i, -1));
        }
        rv.setAdapter(new NewsAdapter(newsList));
        return view;
    }

3.Adapter 和布局文件可以参考GitHub
4.自定义一个 ExtensionModule 继承自 DefaultExtensionModule,复写其中的 getEmoticonTabs() 方法,返回需要展示的 EmoticonTab 列表。

public class MyExtensionModule extends DefaultExtensionModule {
    private RongEmoticonTab rongEmoticon;
    @Override
    public List<IEmoticonTab> getEmoticonTabs() {
        List<IEmoticonTab> emoticonTabs =  super.getEmoticonTabs();
        RongEmoticonTab emojiTab=new RongEmoticonTab();
        emoticonTabs.add(myEmoticon);
        return emoticonTabs;
    }
}

5.在初始化之后,取消 SDK 默认的 ExtensionModule,注册自定义的 ExtensionModule, 如下:

public void setMyExtensionModule() {
    List<IExtensionModule> moduleList = RongExtensionManager.getInstance().getExtensionModules();
    IExtensionModule defaultModule = null;
    if (moduleList != null) {
        for (IExtensionModule module : moduleList) {
            if (module instanceof DefaultExtensionModule) {
                defaultModule = module;
                break;
            }
        }
        if (defaultModule != null) {
            RongExtensionManager.getInstance().unregisterExtensionModule(defaultModule);
            RongExtensionManager.getInstance().registerExtensionModule(new MyExtensionModule());
        }
    }
}

6.如果需要网络下载表情需要下载并持久化表情数据,需要在添加 EmoticonTab 前下载好表情数据。

融云即时通讯SDK集成 — 通知检查

IM即时通讯大兴 发表了文章 • 2 个评论 • 658 次浏览 • 2020-12-02 16:28 • 来自相关话题

背景:最近公司新上的app要加上即时通讯的功能, 自己快速实现一个当然是不可能的了(项目deadline也顶不住哇).就从各家成熟的SDK厂商选来选去的, 各有各的好也各有各的不足.最后点兵点将,选了融云家的SDK(老板说了算hhhh).他家的官网和文档地址:... ...查看全部

微信截图_20201202162632.png背景:

最近公司新上的app要加上即时通讯的功能, 自己快速实现一个当然是不可能的了(项目deadline也顶不住哇).就从各家成熟的SDK厂商选来选去的, 各有各的好也各有各的不足.最后点兵点将,选了融云家的SDK(老板说了算hhhh).
他家的官网和文档地址:
官网:https://www.rongcloud.cn/
文档:https://docs.rongcloud.cn/v4
这个任务当然还是落在我的头上. 集成完毕后, 也踩了不少坑. 所以这篇文章给大家总结下排查融云消息的本地通知和远端推送的办法. 希望可以帮助到正在看这篇文章的你.

什么是本地通知:

当我的App接入了融云的即时通讯sdk后, 便拥有了即时通讯的能力. sdk与融云服务器建立长连接, 当消息发出后, 先走到融云的服务器, 再转发给相应的用户. 这里移动端到服务端, 服务端到移动端, 走的通道都是长连接. 无论你的app是在前台还是在后台, 只要没有被杀死, 那么长连接是一直在的. 所以消息可以即时的发送到达给接收者. 融云把这种走长连接到达的消息, 在通知栏展示的通知叫做本地通知. 也就是消息是顺利发送到接收者端了, 逻辑可以走到消息接收监听那里. 融云sdk内部实现了消息到达后的本地通知, 也赋予了开发者自行实现消息到达后进行本地通知的权利.

本地通知的检查:

这里我总结了一下接入融云sdk后, 关于本地通知接收不顺利的排查. 大致可以分为这么几条:

1.是否设置了 setOnReceiveMessageListener 监听, 并且 onReceived 方法返回的为 true。

 RongIM.setOnReceiveMessageListener(new RongIMClient.OnReceiveMessageListener() {
              @Override
              public boolean onReceived(Message message, 
                  return true;
              }
          });

如果 onReceived 方法返回值为 true 则是监听做了拦截, 则不会走通知逻辑。 2.发送的消息是否是自定义消息。 如果是自定义消息, 则请查看自定义消息的 MessageTag 的注解是否设置了 flag 的值为 MessageTag.ISCOUNTED 或 MessageTag.ISPERSISTED) 。 以下面为例。

@MessageTag(value = "RC:TxtMsg", flag = MessageTag.ISCOUNTED)
  @DestructionTag
  public class TextMessage extends MessageContent {
      ...
  }

如果没有设置其中之一,则不会走通知逻辑。

3.是否设置消息拦截器

设置代码如下:

/**
   * 设置接收消息时的拦截器
   *
   * @param messageInterceptor 拦截器
   */
  public void setMessageInterceptor(new RongIM.getInstance().MessageInterceptor() {
          @Override
          public boolean intercept(final Message message) {
                 return true;
          }
 });

如果设置了消息拦截器, 并且在 intercept() 方法中返回 true, 表示拦截此消息。 则不会走通知逻辑。

4.检查接收到的消息的 senderUserId 是否和当前用户的 id 相同。
如果 id 相同, 则不会走通知逻辑。 自己不可发给自己。

5.查看当前接收的消息是否是属于聊天室会话类型的
聊天室的消息目前不支持本地通知。

6.如果是自定义消息,请检查是否设置了自定义消息的 MessageProvider 。
如果没有,则不会走通知逻辑。

7.接收方本地是否有接收方的用户信息。 如果没有设置, 则不会走通知逻辑。

8.检查应用权限设置,看是否打开应用信任权限, 通知栏权限或者声音提示权限等。如果没有打开,请打开再试。

9.接收方没有发送方的用户信息,接收端不弹通知。用户信息通过用户信息提供者方式获取。
参考链接:https://docs.rongcloud.cn/v3/views/im/ui/guide/private/user/set/android.html

什么是远端推送:

集成了即时通讯的SDK, 我们的app不就能拥有像微信一样随时随地收到消息的即时通讯能力了? 说实话我一开始也是这么认为的. 可惜做开发也要按基本法来, Android平台回收app的这一关咱都过不了, app都给你杀死得透透的了你拿啥收消息呢? 咱又不是微信hhhh. 所以一番急赤白脸地阅读他家文档之后, 才发现app如果活着, 他融云能用自己的通道给你把消息推送到. 如果app被杀死了, 这个消息就在他家的服务端直接交给三方厂商了(也就是五大厂商蓝绿大厂华为小米FCM), 让这条消息走人家厂商的推送通道给送到你手机上.

远端推送的检查:

这里我总结了一下接入融云sdk后, 关于本地通知接收不顺利的排查. 大致可以分为这么几条:

  1. 退出应用的时候,只能调用融云的 disconnect() 方法,而不是 logout()。这样退出后融云才会启动push进程。

  2. push 进程的名字不能更改,必须是默认的名字,既 io.rong.push.
    (2.6.0dev之后的版本,此进程名字可以修改)

  3. 通过 ddms 或者终端里敲入 adb shell ps|grep rong 查看终端里是否存在 io.rong.push 这个进程。

有些手机厂家做了特殊限制,不允许第三方后台进程启动,所以融云的后台进程起不来,导致收不到 push 消息。这种情况可以换个手机测试(如三星,大部分三星手机没有做权限限制)。另外大部分国产手机,有权限设置的菜单,比如小米,华为等,可以手动去安全中心,设置应用的自启动权限,后台运行权限等,就可以收到Push消息了。

不过 vivo 和 oppo 有些型号的手机,一旦应用退到后台,系统会很快把它杀死,这种没有办法解决。目前市面上所有推送都存在这个问题,除非系统把该应用加入白名单。

  1. push 进程存在,仍然收不到 push 消息。

如果你的应用有消息免打扰功能,那么请确认当前登录账号之前是否设置过消息免打扰。如果不太确定,那最好去你的应用设定里重新设置下消息免打扰时间。 这里要注意的是: 如果这个账号之前在别的手机上设置过消息免打扰,换一台手机登录或者卸载重装的时候,融云服务端记录的仍然是之前设置的消息免打扰状态,所以这种情况下是收不到push消息的。


解决融云 SDK 4.0 版本配置 https 导航报 SSLHandshakeException

IM即时通讯大兴 发表了文章 • 0 个评论 • 575 次浏览 • 2020-12-02 16:28 • 来自相关话题

我们公司最近使用融云 IM 进行集成开发. 我们是私有云部署, 导航是通过接口进行设置的. 是 Https 的.之前我们是使用的2.x 版本, 使用下面接口配置一下即可.    RongIMClient.getInsta... ...查看全部

微信截图_20201202162800.png

我们公司最近使用融云 IM 进行集成开发. 我们是私有云部署, 导航是通过接口进行设置的. 是 Https 的.

之前我们是使用的2.x 版本, 使用下面接口配置一下即可.

    RongIMClient.getInstance().enableHttpsSelfCertificate(false);

但是最近我们升级融云到 4.0 版本. 突然发现连接不上了. 日志中报

javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.

这个错误.

于是咨询融云技术支持, 得知 4.0 需要自己进行证书验证.

需在 application 中添加即可.

 try {
    //使用X509TrustManager代替CertificateTrustManager跳过证书验证。
    TrustManager tm[] =  new X509TrustManager() {
                @Override
                public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                }
                @Override
                public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                }
                @Override
                public X509Certificate[] getAcceptedIssuers() {
                    return new X509Certificate[0];
                }
            }
        };
        SSLContext context = SSLContext.getInstance("TLS");   
        context.init(null, tm, null);
        SSLUtils.setSSLContext(context);
        SSLUtils.setHostnameVerifier(new HostnameVerifier() {
            @Override
            public boolean verify(String hostname, SSLSession session) {
                return true;
             }
            });
} catch (Throwable e) {
    throw new IllegalStateException(e);
}

注意的是, 由于导航请求是在融云的 ipc 进程中的, 所以在 application 中设置上面代码的时候, 千万不要设置在主进程中. 否则无效.

在融云 IMkit 会话界面基础上添加消息已读未读.

WebRTC大兴 发表了文章 • 0 个评论 • 714 次浏览 • 2020-11-06 15:37 • 来自相关话题

使用过融云的同学们可能知道. 融云 IMkit 的会话界面,  发送玩消息后, 如果对方已读, 发送端则会显示小对号的图片. 但是更具需求要把小对号改为已读未读. 接下来我们就一块实现这个功能.打开回执功能首先, 要确定打开融云的消息回执功能. 这个... ...查看全部

使用过融云的同学们可能知道. 融云 IMkit 的会话界面,  发送玩消息后, 如果对方已读, 发送端则会显示小对号的图片. 但是更具需求要把小对号改为已读未读. 接下来我们就一块实现这个功能.

打开回执功能

首先, 要确定打开融云的消息回执功能. 这个很简单, 就是在 rc_config.xml 中把下面属性配置为 true 即可.

<!-- 设置是否开启消息回执, true 为打开, false 为关闭--><bool name="rc_read_receipt">true</bool>

这样发送消息后, 对方已读, 发送端就会出现融云默认的小对号了.

自定义 Adapter

出现小对号后, 下一步就是要进行对融云适配器的改造了.

第一步
创建 CustomMessageListAdapter 继承 MessageListAdapter. 根据自己的需求复写 newView() 或者 bindView() 方法.

class CustomMessageListAdapter extends MessageListAdapter {    @Override    protected void bindView(View v, final int position, final UIMessage data) {        // 此方法中操作控件    }}

第二步

创建 CustomConversationFragment 继承于 ConversationFragment, 并复写父类中的 onResolveAdapter(), 返回 CustomMessageListAdapter 对象.

 class CustomMessageListAdapter extends ConversationFragment {        @Override        public MessageListAdapter onResolveAdapter() {            return new CustomMessageListAdapter();        }    }

第三步

使用 CustomConversationListFragment 代替 ConversationListFragment 进行配置使用即可.

u=2322698630,4022941591&fm=26&gp=0.jpg

添加已读未读

自定了了 Adapter, 我们就可以在原先逻辑的基础上进行扩展了. 现在主要的任务就是找到小对号的控件.

我们先看一下默认的 item 布局 rc_item_message.xml. 在布局中我们可找到下面的控件

    <TextView        android:id="@id/rc_read_receipt"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_gravity="bottom"        android:layout_marginRight="4dp"        android:drawableLeft="@drawable/rc_read_receipt"        android:drawableStart="@drawable/rc_read_receipt"        android:textColor="@color/rc_read_receipt_status"        android:textSize="12sp"        android:visibility="gone" />

这个就是显示对号的控件了.  是一个 TextView 的控件, 由于我们是添加 “已读” 、“未读” 字符串, 正好可以直接使用这个 TextView 控件.

我们通过控件的 id 在 Adapter 中找到控件的名称.

class CustomMessageListAdapter extends MessageListAdapter {    @Override    protected void bindView(View v, final int position, final UIMessage data) {         super(v, position, data);       // 此方法中操作控件       final ViewHolder holder = (ViewHolder) v.getTag();       if (data.getMessageDirection() == Message.MessageDirection.SEND) {            if (readRec && data.getSentStatus() == Message.SentStatus.READ) {                if (data.getConversationType().equals(Conversation.ConversationType.PRIVATE) && tag.showReadState()) {                    holder.readReceipt.setVisibility(View.VISIBLE);                    holder.readReceipt.setText(已读);                    holder.readReceipt.setBackground(null);                } else {                      holder.readReceipt.setText(未读);                      holder.readReceipt.setBackground(null);                      holder.readReceipt.setVisibility(View.VISIBLE);            }        }    }}

这样就会显示就会显示已读未读的字样了.


融云集成之避坑指南-Android推送篇

WebRTC大兴 发表了文章 • 0 个评论 • 657 次浏览 • 2020-11-06 15:37 • 来自相关话题

在集成推送时候,需要先了解融云针对推送的定义: Push 通知是当接收方的主进程被杀死或者回收,或者您主动调用 disconnect() 的操作, 导致 IM 长连接通道与服务器断开后。 在这种情况下,会收到Push 服务;说白了,就是应用进程被杀掉,才会收到... ...查看全部

在集成推送时候,需要先了解融云针对推送的定义: Push 通知是当接收方的主进程被杀死或者回收,或者您主动调用 disconnect() 的操作, 导致 IM 长连接通道与服务器断开后。 在这种情况下,会收到Push 服务;

说白了,就是应用进程被杀掉,才会收到push 的,否则的话,是会走融云内部的长链接通道的;若是连应用进程都不了解的话,我要拿起我四十米的大刀了。
avatar

好了,前提介绍结束,接下来,我们正式开始避坑。

融云的推送分为俩大类:RongPush 以及其他厂商推送;至于为什么会有厂商推送 ,主要书由于众说周知的原因,各个手机厂商针对系统的room 太深,导致融云的push 进程服务无法自启动以及无法常存与服务后台,所以为了满足离线时候,push 的到达率,所以接入了主流的厂商推送服务,至于你说,为什么不接入统一推送联盟,估计等推送联盟孵化出来,可能得等到2045年了;哈哈,估计我再不开始介绍接入,大家就要发飙了。
avatar

各位客官,稍安勿躁,马上开始。本篇我们先介绍厂商推送之华为推送过程中遇到的坑;

首先,按照融云官网的描述,融云SDK 目前是有俩个版本,2.x 版本的SDK 以及4.x 版本的SDK ,分别对应的是华为的2.x 的jar 以及4.x 的jar ,可以分别参考 2.x版本 以及4.x版本,按照文档进行集成即可,具体集成过程按照文档集成即可,此处无需多说;

接下来,先介绍我在集成2.x 版本时候遇到的问题:

1.出现的问题一,没有填写对应的应用标识;(此处是我的锅,没有细致的看文档);

按照融云文档的描述,
avatar 此处要填写对应的华为的appkey 等数据的,而愚蠢的我竟然没有填写对应的 AppIDAppSecret,希望各位 大大在集成过程中不要跟我一样犯傻;

2.出现的问题二,按照文档配置完成了,结果,还是收不到推送,不会进行排查;

似乎这个问题,很多小伙伴都遇到了,所以,我在融云知识库中找到了一篇官方提供的排查手段,仅供大家参考 https://support.rongcloud.cn/ks/ODg0 ,若是大家还是搞不定,可以直接找融云进行技术支持的;

以上是在集成2.x 版本时候遇到的问题,不算特别困难,也是比较轻松的解决了;

但是,事情万万没有想到的那么简单,时间过去一个月,项目要升级4.x 的融云SDK ,于是,华为推送也要进行对应的升级,于是我开开心心准备认真参谋一下4.x文档,还好,文档比较简单;

于是,对应的问题,也出现了:

问题1:在升级过程中,并没有找见 agconnect-servics.json  文件的下载地址。

经过与融云技术人员的沟通,在华为提供5.0版本的推送服务之后,agconnect-servics.json  下载位置改变了,所以我将我知道的下载路径提供给大家,在华为开发者平台-我的项目中-对应的项目下载即可。

问题2: 由于我是从2.x 升级到4.x 的,所以,眼瞎的我没有看清楚文档中AndroidMainfest 中的配置是要删除的,所以导致找不到文件了,所以请大家一定要看清楚标题,是删除配置,不是添加配置;

问题3: 在EMUI 10以上 是可以收到推送的,但是在EMUI 10以下,收不到推送。

这个问题经过与融云技术人员以及华为技术人员的沟通可以确定,由于华为不再继续维护2.0推送服务,改推4.0 以上的服务,但是4.0的推送服务,又对HMSCore 是由要求的,所以只能升级用户的 HMSCore服务,不过融云对外提供了升级监听,具体可以参考融云文档


iOS 性能优化:优化 App 启动速度

IM即时通讯大神庵 发表了文章 • 0 个评论 • 203 次浏览 • 2020-10-26 12:00 • 来自相关话题

作者:Damonwong,iOS 开发者来源公众号丨老司机技术周报(ID:LSJCoding)Sessions: https://developer.apple.com/videos/play/wwdc2019/423/苹果是一家特别注重用户体验的公司,过去几... ...查看全部

作者:Damonwong,iOS 开发者

来源公众号丨老司机技术周报(ID:LSJCoding)

Sessions: https://developer.apple.com/videos/play/wwdc2019/423/

苹果是一家特别注重用户体验的公司,过去几年一直在优化 App 的启动时间,特别是去年的 WWDC 2019 keynote[1] 上提到,在过去一年苹果开发团队对启动时间提升了 200%

虽然说是提升了 200%,但是有些问题还是没有说清楚,比如:

  • 为什么优化了这么多时间?

  • 作为开发者的我们,我们还可以做哪些针对启动速度的优化?

所以我们今天结合 WWDC2019 - 423 - Optimizing App Launch[2] 聊一下和启动相关的东西

名词解释

先介绍一些和启动相关的名词。

Mach-O

Mach-O 是 iOS 系统不同运行时期可执行的文件的文件类型统称。主要分以下三类:

  • Executable - 可执行文件,是 App 中的主要二进制文件

  • Dylib - 动态库,在其他平台也叫 DSO 或者 DLL

  • Bundle - 苹果平台特有的类型,是无法被连接的 Dylib。只能在运行时通过 dlopen() 加载

Mach-O 的基本结构如下图所示,分为三个部分:

  • Header 包含了 Mach-O 文件的基本信息,如 CPU 架构,文件类型,加载指令数量等

  • Load Commands 是跟在 Header 后面的加载命令区,包含文件的组织架构和在虚拟内存中的布局方式,在调用的时候知道如何设置和加载二进制数据

  • Data 包含 Load Commands 中需要的各个 Segment 的数据。

绝大多数 Mach-O 文件包括以下三种 Segment

  • __TEXT - 代码段,包括头文件、代码和常量。只读不可修改。

  • __DATA - 数据段,包括全局变量, 静态变量等。可读可写。

  • __LINKEDIT - 如何加载程序, 包含了方法和变量的元数据(位置,偏移量),以及代码签名等信息。只读不可修改。

Image

指的是 Executable,Dylib 或者 Bundle 的一种。

Framework

有很多东西都叫做 Framework,但在本文中,Framework 指的是一个 dylib,它周围有一个特殊的目录结构来保存该 dylib 所需的文件。

虚拟内存(Virtual Memory)

虚拟内存是建立在物理内存和进程之间的中间层。是一个连续的逻辑地址空间,而且逻辑地址可以没有对应的实际物理内存地址,也可以让多个逻辑地址对应到一个物理内存地址上。

Page Fault

当进程访问一个没有对应物理地址的逻辑地址时,会发生 Page Fault

Lazy Reading

某个想要读取的页没有在内存中就会触发 Page Fault,系统通过调用 mmap() 函数读取指定页,这个过程叫做 Lazy Reading

COW(Copy-On-Write)

当进程需要对某一页内容进行修改时,内核会把需要修改的部分先复制一份,然后再修改,并把逻辑地址重新映射到新的物理内存去。这个过程叫做 Copy-On-Write

Dirty Page & Clean Page

Image 加载后,被修改过内容的 Page 叫做 Dirty Page,会包含着进程特定的信息。与之相对的叫 Clean Page,可以从磁盘重新生成。

共享内存(Share RAM)

当多个 Mach-O 都依赖同一个 Dylib(eg. UIKit)时,系统会让这几个 Mach-O 的调用 Dylib 的逻辑地址都指向同一块物理内存区域,从而实现内存共享。Dirty Page 为进程独有,不能被共享。

地址空间布局随机化(ASLR)

当 Image 加载到逻辑地址空间的时候,系统会利用 ASLR 技术,使得 Image 的起始地址总是随机的,以避免黑客通过起始地址+偏移量找到函数的地址

当系统利用 ASLR 分配了随机地址后,从 0 到该地址的整个区间会被标记为不可访问,意味着不可读,不可写,不可被执行。这个区域就是 __PAGEZERO 段,它的大小在 32 位系统是 4KB+,而在 64 位系统是 4GB+

代码签名(Code Sign)

代码签名可以让 iOS 系统确保要被加载的 Image 的安全性,用 Code Sign 设置签名时,每页内容都会生成一个单独的加密散列值,并存储到 __LINKEDIT 中去,系统在加载时会校验每页内容确保没有被篡改。

dyld(dynamic loader)

dyld 是 iOS 上的二进制加载器,用于加载 Image。有不少人认为 dyld 只负责加载应用依赖的所有动态链接库,这个理解是错误的。dyld 工作的具体流程如下:参考:dyld启动流程[3]

Load dylibs

dyld 在加载 Mach-O 之前会先解析 Header 和 Load Commands, 然后就知道了这个 Mach-O 所依赖的 dylibs,以此类推,通过递归的方式把全部需要的 dylib 都加载进来。

一般来说,一个 App 所依赖的 dylib 在 100 - 400 左右,其中大多数都是系统的 dylib,因为有缓存和共享的缘故,读取速度比较高。

Fix-ups

因为 ASLR 和 Code Sign 的原因,刚被加载进来的 dylib 都处于相对独立的状态,为了把它们绑定起来,需要经过一个 Fix-ups 过程。Fix-ups 主要有两种类型:Rebase 和 Bind。

PIC(Position Independent Code)

因为代码签名的原因,dyld 无法直接修改指令,但是为了实现在运行时可以 Fix-ups,在 code gen 时,通过动态 PIC(Position Independent Code)技术,使本来因为代码签名限制不能再修改的代码,可以被加载到间接地址上。当要调用一个方法时,会先在 __DATA 段中建立一个指针指向这个方法,再通过这个指针实现间接调用。

Rebase

Rebase 就是针对“因为 ASLR 导致 Mach-O 在加载到内存中是一个随机的首地址”这一个问题做一个数据修正的过程。会将内部指针地址都加上一个偏移量,偏移量的计算方法如下:

  Slide = actual_address - preferred_address

所有需要 Rebase 的指针信息已经被编码到 __LINKEDIT 里。然后就是不断重复地对 __DATA 中需要 Rebase 的指针加上这个偏移量。这个过程中可能会不断发生 Page Fault 和 COW,从而导致 I/0 的性能损耗问题,不过因为 Rebase 处理的是连续地址,所以内核会预先读取数据,减少 I/O 的消耗。

Binding

Binding 就是对调用的外部符号进行绑定的过程。比如我们要使用到 UITableView,即符号 _OBJC_CLASS_$_UITableView, 但这个符号又不在 Mach-O 中,需要从 UIKit.framework 中获取,因此需要通过 Binding 把这个对应关系绑定到一起。

在运行时,dyld 需要找到符号名对应的实现。而这需要很多计算,包括去符号表里找。找到后就会将对应的值记录到 __DATA 的那个指针里。Binding 的计算量虽然比 Rebasing 更多,但实际需要的 I/O 操作很少,因为之前 Rebasing 已经做过了。

dyld2 & dyld3

 

在 iOS 13 之前,所有的第三方 App 都是通过 dyld 2 来启动 App 的,主要过程如下:

  • 解析 Mach-O 的 Header 和 Load Commands,找到其依赖的库,并递归找到所有依赖的库

  • 加载 Mach-O 文件

  • 进行符号查找

  • 绑定和变基

  • 运行初始化程序

上面的所有过程都发生在 App 启动时,包含了大量的计算和I/O,所以苹果开发团队为了加快启动速度,在 WWDC2017 - 413 - App Startup Time: Past, Present, and Future[4] 上正式提出了 dyld3。

dyld3 被分为了三个组件:

  • 一个进程外的 MachO 解析器


    • 预先处理了所有可能影响启动速度的 search path、@rpaths 和环境变量

    • 然后分析 Mach-O 的 Header 和依赖,并完成了所有符号查找的工作

    • 最后将这些结果创建成了一个启动闭包

    • 这是一个普通的 daemon 进程,可以使用通常的测试架构

  • 一个进程内的引擎,用来运行启动闭包


    • 这部分在进程中处理

    • 验证启动闭包的安全性,然后映射到 dylib 之中,再跳转到 main 函数

    • 不需要解析 Mach-O 的 Header 和依赖,也不需要符号查找。

  • 一个启动闭包缓存服务


    • 系统 App 的启动闭包被构建在一个 Shared Cache 中, 我们甚至不需要打开一个单独的文件

    • 对于第三方的 App,我们会在 App 安装或者升级的时候构建这个启动闭包。

    • 在 iOS、tvOS、watchOS中,这这一切都是 App 启动之前完成的。在 macOS 上,由于有 Side Load App,进程内引擎会在首次启动的时候启动一个 daemon 进程,之后就可以使用启动闭包启动了。

dyld 3 把很多耗时的查找、计算和 I/O 的事前都预先处理好了,这使得启动速度有了很大的提升。

App 启动

介绍完上面这一大堆名词之后,我们正式进入主题。

App 启动为什么这么重要?

  • App 启动是和用户的第一个交互过程,所以要尽量缩短这个过程的时间,给用户一个良好的第一印象

  • 启动代表了你的代码的整体性能,如果启动的性能不好,其他部分的性能可能也不会太好

  • 启动会占用 CPU 和内存,从而影响系统性能和电池

所以我们要好好优化启动时间。

启动类型

App 的启动类型分为三类

  • Cold Launch 也就是冷启动,冷启动需要满足以下几个条件:


    • 重启之后

    • App 不在内存中

    • 没有相关的进程存在

  • Warm Launch 也就是热启动,热启动需要满足以下几个条件:


    • App 刚被终止

    • App 还没完全从内存中移除

    • 没有相关的进程存在

  • Resume Launch 指的是被挂起的 App 继续的过程,需要满足以下几个条件:


    • App 被挂起

    • App 还全部都在内存中

    • 还存在相关的进程

App 启动阶段

App 启动分为三个阶段

  • 初始化 App 的准备工作

  • 绘制第一帧 App 的准备工作及绘制(这里的第一帧并不是获取到数据之后的第一帧,可以是一张占位视图),这时候用户与App已经可以交互了,比如 tabbar 切换

  • 获取到页面的所有数据之后的完整的绘制第一帧页面

在这个地方,苹果再次强调了一下,建议「用户从点击 App 图标到可以再次交互,也就是第二阶段结束」的时间最好在 400ms 以内。目前来看,大部分 App 都没有达到这个目标。

下面,我们把上面三个阶段分成下面这 6 个部分,讲一下这几个阶段做了什么以及有什么可以优化的地方。

 

System Interface

初始化 App 的准备工作,系统主要做了两个事情:Load dylibs 和 libSystem init

在 2017 年苹果介绍过 dyld3 给系统 App 带来了多少优化,今年 dyld3 正式开发给开发者使用,这意味着 iOS 系统会将你热启动的运行时依赖给缓存起来。以达到减少启动时间的目的。这也就是提升 200% 的原因之一。

视频中只说优化了热启动时间,理论上对于 iOS 系统来说 dyld3 应该还可以优化冷启动时间,所以不知道是因为给 iPad 增加了多任务功能的原因,还是没有把所有功能开放的原因,作者只提了热启动这个原因暂时还不太清楚。

除此之外,在 Load dylibs 阶段,开发者还可以做以下优化:

  • 避免链接无用的 frameworks,在 Xcode 中检查一下项目中的「Linked Frameworks and Librares」部分是否有无用的链接。

  • 避免在启动时加载动态库,将项目的 Pods 以静态编译的方式打包,尤其是 Swift 项目,这地方的时间损耗是很大的。

  • 硬链接你的依赖项,这里做了缓存优化。

也许有人会困惑是不是使用了 dyld3 了,我们就不需要做 Static Link 了,其实还是需要的,感兴趣的可以看一下 Static linking vs dyld3[5] 这篇文章,里面有一个详细的数据对比。

libSystem init 部分,主要是加载一些优先级比较低的系统组件,这部分时间是一个固定的成本,所以我们开发人员不需要关心。

Static Runtime Initializaiton

这个阶段主要是 Objective-C 和 Swift Runtime 的初始化,会调用所有的 +load 方法,将类的信息注册到 runtime 中。

在这个阶段,原则上不建议开发者做任何事情,所以为了避免一些启动时间的损耗,你可以做以下几个事情:

  • 在 framework 开发时,公开专有的初始化 API

  • 减少在 +load 中做的事情

  • 使用 initialize 进行懒加载初始化工作

UIKit Initializaiton

这个阶段主要做了两个事情:

  • 实例化 UIApplication 和 UIApplicationDelegate

  • 开始事件处理和系统集成

所以这个阶段的优化也比较简单,你需要做两个事情:

  • 最大限度的减少 UIApplication 子类初始化时候的工作,更甚至与不子类化 UIApplication

  • 减少 UIApplicationDelegate 的初始化工作

Application Initializaiton

这个阶段主要是生命周期方法的回调,也正是开发者最熟悉的部分。

调用 UIApplicationDelegate 的 App 生命周期方法:

  application:willFinishLaunchingWithOptions: 
  application:didFinishLaunchingWithOptions:

和 UIApplicationDelegate 的 UI 生命周期方法:

  applicationDidBecomeActive:

同时,iOS 13 针对 UISceneDelegate 增加了新的回调:

  scene:willConnectToSession:options:
  sceneWillEnterForeground:
  sceneDidBecomeActive:

也会在这个阶段调用。感兴趣的可以关注一下 Getting the Most out of Multitasking 这个 Session,暂时没有视频资源,怀疑是现场演示翻车了,所以没有把视频资源放出来。

在这个阶段,开发者可以做的优化:

  • 推迟和启动时无关的工作

  • Senens 之间共享资源

Fisrt Frame Render

这个阶段主要做了创建、布局和绘制视图的工作,并把准备好的第一帧提交给渲染层渲染。会频繁调用以下几个函数:

 loadView
 viewDidLoad 
 layoutSubviews

在这个阶段,开发者可以做的优化:

  • 减少视图层级,懒加载一些不需要的视图

  • 优化布局,减少约束

更多细节可以从 WWDC2018 - 220 - High Performance Auto Layout[6] 中了解

Extend

大部分 App 都会通过异步的方式获取数据,并最终呈现给用户。我们把这一部分称为 Extend。

因为这一部分每个 App 的表现都不一样,所以苹果建议开发者使用 os_signpost 进行测量然后慢慢分析慢慢优化。

测量 App 启动时间

要找到启动过程中的问题,就要进行多次测量并前后比较。但是如果变量没有控制好,就会导致误差。

所以为了保证测量的数据能够真实的反应问题,我们要减少不稳定性因素,保证在可控的相近的环境下进行测量。最后使用一致的结果来分析。

条件一致性

为了保证环境一致,我们可以做下面这几个事情:

  • 重启手机,并等待 2-3 分钟

  • 启用飞行模式或者使用模拟网络

  • 不使用或者不变更 iCloud 的账户

  • 使用 release 模式进行 build

  • 测量热启动时间

iColud 账户切换会影响性能,所以不要切换账号或者不开启 iCloud。

测量注意点

  • 尽可能的使用具有代表性的数据进行测试

    如果不使用具有代表性的数据进行测试,就会出现偏差

  • 使用不同的新旧设备进行测试

  • 最后你还可以使用 XCTest 来测试,多运行几次,取平均结果

关于使用 XCTest 测试启动时间的信息,可以看一下 WWDC2019 - 417 - Improving Battery Life and Performance[7],但是我测试了一下,目前好像还有一部分 API 还没有开放出来,暂时还不能使用。

使用 Instruments 分析和优化 App 启动过程

优化方式

苹果给了我们三个优化方式的建议,整体思想和前面提到的各个阶段的优化差不多

Minimize Work

  • 推迟与第一帧无关的工作

  • 从主线程移开阻塞工作

  • 减少内存使用量

Prioritize Work

  • 定义好任务的优先级。

  • 利用好 GCD 来优化你的启动速度。

  • 让重要的事情保持优先

更深入的了解有关 GCD 的内容,可以看一下 WWDC2017 - 706 - Modernizing Grand Central Dispatch Usage[8]

Optimize Work

  • 简化现有工作,比如只请求必要的数据。

  • 优化算法和数据结构

  • 缓存资源和计算

使用 Instruments 分析 App 启动过程

当知道如何优化之后,我们需要针对我们的启动过程进行分析。Xcode 11 的 Instruments 为此新增了一个 App launch 模板,让开发者可以更好的分析自己 App 的启动速度。

 

运行后可以看到各个阶段的具体时间,根据数据进行优化,还能看到耗时的函数调用。

 

系统优化

去年苹果做了很多优化,下面这几个高亮的是和启动速度有关的优化

 

但是不知道是不是时间原因,在 session 中对于这部分的解释特别少,很难理解 200% 到底做了什么。

但是 Craig Federighi 在 The Talk Show Live From WWDC 2019, With Craig Federighi and Greg Joswiak[9] 中针对为什么优化了 200% 说了这样一段话:

Isn’t that crazy that was quite a discovery for us. No it turns out that over times as in terms of the way the apps were encrypted and the way fair play worked and so forth. The encryption became part of the critical path actually of launching the apps. I mean the processors are capable or up and through the thing that actually it was a problem. And then there are other optimizations that based on what was visible to system at certain things. And so it actually cut out optimization opportunities and so when we really identified that opportunity we said okay. We can actually come up with better format that’s gonna eliminate that being on the critical path, It’s going to enable all these pre-binding things. And then we did a whole bunch of other work to optimize the objective-c runtime to optimize the linker the dynamic linker a bunch of other things and you put it all together. And yeah that I mean a cold launch this is we’ve never had a win like this to launch time in a single release.

从这段话中,除了 dyld3 的功劳之外,减少对代码签名加密也是优化之一。

监控线上用户 App 的启动

Xcode 11 在 Xcode Organizer 新增了一个监控面板,在这个面板里面可以查看多个维度的用户数据,其中还包括平均启动时间。

 

当你通过 Instruments 分析完你的启动过程,并做了大量优化之后,你就可以通过 Xcode Organizer 来分析你这次优化效果到底怎么样。

当然你可以通过去年新出的 MetricKit[10] 获取一些自定义的数据,具体参照 WWDC2019 - 417 -Improving Battery Life and Performance[11]

总结

今年苹果提的很多优化建议其实在早几年都已经说过一遍了,属于老生常谈了。但是值得点赞的是新出的 Instruments 的 App launch 和 Xcode Organizer 的性能监控。

启动优化是一个需要经常反复做的事情,越早发现问题,越容易解决问题,如果你想给你的用户一个良好的第一印象,就赶快行动起来吧。

参考资料

[1]

WWDC 2019 keynote: https://developer.apple.com/videos/play/wwdc2019/101/

[2]

WWDC2019 - 423 - Optimizing App Launch: https://developer.apple.com/videos/play/wwdc2019/423/

[3]

dyld启动流程: https://leylfl.github.io/2018/05/28/dyld启动流程/

[4]

WWDC2017 - 413 - App Startup Time: Past, Present, and Future: https://developer.apple.com/videos/play/wwdc2017/413/

[5]

Static linking vs dyld3: https://allegro.tech/2018/05/Static-linking-vs-dyld3.html

[6]

WWDC2018 - 220 - High Performance Auto Layout: https://developer.apple.com/videos/play/wwdc2018/220/

[7]

WWDC2019 - 417 - Improving Battery Life and Performance: https://developer.apple.com/videos/play/wwdc2019/417/

[8]

WWDC2017 - 706 - Modernizing Grand Central Dispatch Usage: https://developer.apple.com/videos/play/wwdc2017/706/

[9]

The Talk Show Live From WWDC 2019, With Craig Federighi and Greg Joswiak: https://daringfireball.net/2019/06/the_talk_show_live_from_wwdc_2019

[10]

MetricKit: https://developer.apple.com/documentation/metrickit

[11]

WWDC2019 - 417 -Improving Battery Life and Performance: https://developer.apple.com/videos/play/wwdc2019/417/


融云 IM SDK 集成 —- 刷新会话界面和会话列表界面

IM即时通讯大兴 发表了文章 • 0 个评论 • 678 次浏览 • 2020-12-02 16:28 • 来自相关话题

最近集成融云 IMkit 发现, 融云 IMkit 提供的 ConversationListFragment 和 ConversationFragment 都没有提供刷新方法.我们有个需求是, 使用 Message 的 extra 修改值后, 但是界面没有进行... ...查看全部

最近集成融云 IMkit 发现, 融云 IMkit 提供的 ConversationListFragment 和 ConversationFragment 都没有提供刷新方法.

微信截图_20201202162201.png

我们有个需求是, 使用 Message 的 extra 修改值后, 但是界面没有进行刷新, 需要自己调用代码进行刷新, 但是, 融云没有提供对应的刷新界面的方法, 所以只能看代码.

刷新 ListView 是需要获取 Adapter , 然后使用 Adapter 进行刷新, 但是我们无法从 ConversationListFragment 中直接获取, 那怎么办呢?

我们知道, ListView 中有 getAdapter 的方法. 那我们能不能先获取 ListView 控件呢? 答案是可以的. 我们通过 findById 方法即可获取 ListView. 通过查找, 我们可以知道, 融云会话列表的 listView 的 id 为 R.id.rc_list.

所以代码如下.

ListView mList = (ListView)findViewById(R.id.rc_list);
HeaderViewListAdapter adapter = (HeaderViewListAdapter)mList.getAdapter();
ConversationListAdapter wrappedAdapter = (ConversationListAdapter)adapter.getWrappedAdapter();
if (adapter != null) {
    wrappedAdapter.notifyDataSetChanged();
}


融云即时通讯SDK集成 — 通知检查

IM即时通讯大兴 发表了文章 • 2 个评论 • 658 次浏览 • 2020-12-02 16:28 • 来自相关话题

背景:最近公司新上的app要加上即时通讯的功能, 自己快速实现一个当然是不可能的了(项目deadline也顶不住哇).就从各家成熟的SDK厂商选来选去的, 各有各的好也各有各的不足.最后点兵点将,选了融云家的SDK(老板说了算hhhh).他家的官网和文档地址:... ...查看全部

微信截图_20201202162632.png背景:

最近公司新上的app要加上即时通讯的功能, 自己快速实现一个当然是不可能的了(项目deadline也顶不住哇).就从各家成熟的SDK厂商选来选去的, 各有各的好也各有各的不足.最后点兵点将,选了融云家的SDK(老板说了算hhhh).
他家的官网和文档地址:
官网:https://www.rongcloud.cn/
文档:https://docs.rongcloud.cn/v4
这个任务当然还是落在我的头上. 集成完毕后, 也踩了不少坑. 所以这篇文章给大家总结下排查融云消息的本地通知和远端推送的办法. 希望可以帮助到正在看这篇文章的你.

什么是本地通知:

当我的App接入了融云的即时通讯sdk后, 便拥有了即时通讯的能力. sdk与融云服务器建立长连接, 当消息发出后, 先走到融云的服务器, 再转发给相应的用户. 这里移动端到服务端, 服务端到移动端, 走的通道都是长连接. 无论你的app是在前台还是在后台, 只要没有被杀死, 那么长连接是一直在的. 所以消息可以即时的发送到达给接收者. 融云把这种走长连接到达的消息, 在通知栏展示的通知叫做本地通知. 也就是消息是顺利发送到接收者端了, 逻辑可以走到消息接收监听那里. 融云sdk内部实现了消息到达后的本地通知, 也赋予了开发者自行实现消息到达后进行本地通知的权利.

本地通知的检查:

这里我总结了一下接入融云sdk后, 关于本地通知接收不顺利的排查. 大致可以分为这么几条:

1.是否设置了 setOnReceiveMessageListener 监听, 并且 onReceived 方法返回的为 true。

 RongIM.setOnReceiveMessageListener(new RongIMClient.OnReceiveMessageListener() {
              @Override
              public boolean onReceived(Message message, 
                  return true;
              }
          });

如果 onReceived 方法返回值为 true 则是监听做了拦截, 则不会走通知逻辑。 2.发送的消息是否是自定义消息。 如果是自定义消息, 则请查看自定义消息的 MessageTag 的注解是否设置了 flag 的值为 MessageTag.ISCOUNTED 或 MessageTag.ISPERSISTED) 。 以下面为例。

@MessageTag(value = "RC:TxtMsg", flag = MessageTag.ISCOUNTED)
  @DestructionTag
  public class TextMessage extends MessageContent {
      ...
  }

如果没有设置其中之一,则不会走通知逻辑。

3.是否设置消息拦截器

设置代码如下:

/**
   * 设置接收消息时的拦截器
   *
   * @param messageInterceptor 拦截器
   */
  public void setMessageInterceptor(new RongIM.getInstance().MessageInterceptor() {
          @Override
          public boolean intercept(final Message message) {
                 return true;
          }
 });

如果设置了消息拦截器, 并且在 intercept() 方法中返回 true, 表示拦截此消息。 则不会走通知逻辑。

4.检查接收到的消息的 senderUserId 是否和当前用户的 id 相同。
如果 id 相同, 则不会走通知逻辑。 自己不可发给自己。

5.查看当前接收的消息是否是属于聊天室会话类型的
聊天室的消息目前不支持本地通知。

6.如果是自定义消息,请检查是否设置了自定义消息的 MessageProvider 。
如果没有,则不会走通知逻辑。

7.接收方本地是否有接收方的用户信息。 如果没有设置, 则不会走通知逻辑。

8.检查应用权限设置,看是否打开应用信任权限, 通知栏权限或者声音提示权限等。如果没有打开,请打开再试。

9.接收方没有发送方的用户信息,接收端不弹通知。用户信息通过用户信息提供者方式获取。
参考链接:https://docs.rongcloud.cn/v3/views/im/ui/guide/private/user/set/android.html

什么是远端推送:

集成了即时通讯的SDK, 我们的app不就能拥有像微信一样随时随地收到消息的即时通讯能力了? 说实话我一开始也是这么认为的. 可惜做开发也要按基本法来, Android平台回收app的这一关咱都过不了, app都给你杀死得透透的了你拿啥收消息呢? 咱又不是微信hhhh. 所以一番急赤白脸地阅读他家文档之后, 才发现app如果活着, 他融云能用自己的通道给你把消息推送到. 如果app被杀死了, 这个消息就在他家的服务端直接交给三方厂商了(也就是五大厂商蓝绿大厂华为小米FCM), 让这条消息走人家厂商的推送通道给送到你手机上.

远端推送的检查:

这里我总结了一下接入融云sdk后, 关于本地通知接收不顺利的排查. 大致可以分为这么几条:

  1. 退出应用的时候,只能调用融云的 disconnect() 方法,而不是 logout()。这样退出后融云才会启动push进程。

  2. push 进程的名字不能更改,必须是默认的名字,既 io.rong.push.
    (2.6.0dev之后的版本,此进程名字可以修改)

  3. 通过 ddms 或者终端里敲入 adb shell ps|grep rong 查看终端里是否存在 io.rong.push 这个进程。

有些手机厂家做了特殊限制,不允许第三方后台进程启动,所以融云的后台进程起不来,导致收不到 push 消息。这种情况可以换个手机测试(如三星,大部分三星手机没有做权限限制)。另外大部分国产手机,有权限设置的菜单,比如小米,华为等,可以手动去安全中心,设置应用的自启动权限,后台运行权限等,就可以收到Push消息了。

不过 vivo 和 oppo 有些型号的手机,一旦应用退到后台,系统会很快把它杀死,这种没有办法解决。目前市面上所有推送都存在这个问题,除非系统把该应用加入白名单。

  1. push 进程存在,仍然收不到 push 消息。

如果你的应用有消息免打扰功能,那么请确认当前登录账号之前是否设置过消息免打扰。如果不太确定,那最好去你的应用设定里重新设置下消息免打扰时间。 这里要注意的是: 如果这个账号之前在别的手机上设置过消息免打扰,换一台手机登录或者卸载重装的时候,融云服务端记录的仍然是之前设置的消息免打扰状态,所以这种情况下是收不到push消息的。


融云im小程序集成初体验

IM即时通讯大神庵 发表了文章 • 0 个评论 • 61 次浏览 • 2021-04-15 15:15 • 来自相关话题

最近领导让做一个小程序端的聊天功能,基于多方考虑,最终选择了融云的im来实现,那就先写个demo体验下吧。首先呢,当然是查看官方文档,文档地址如下: https://docs.rongcloud.cn/v4/views/im/noui/guide/p... ...查看全部

最近领导让做一个小程序端的聊天功能,基于多方考虑,最终选择了融云的im来实现,那就先写个demo体验下吧。

微信截图_20210415151418.png

首先呢,当然是查看官方文档,文档地址如下: https://docs.rongcloud.cn/v4/views/im/noui/guide/private/setting/include/mini.html

需要注意要在管理后台的小程序服务下将小程序的开关打开,否则会报错,相关的demo也可从后端下载到。

拿到sdk了,我们就可以上手集成了,嘻嘻……

由于是demo,暂时先把appkey和token配置写死了,放在了config.js中,appkey和token可以从管理后台获取到。

其次呢,我们需要把im引入,可以整一个公用的js文件,在此引入im sdk,初始化一个全局的im对象(在此呢需要注意im对象一定是全局的,不能赋值给小程序里面的data值,之前我就犯了错,赋值给了data,导致页面只要一调用setData方法就一直报Converting string to Json的错误,大家可以在集成过程中避雷呢)。

引入代码如下:

const RongIMLib = require("./RongIMLib-3.0.7-dev.js");
const config = require("../public/config");
const imInstance = RongIMLib.init({ appkey: config.appkey});
export {
  RongIMLib,
  config,
  imInstance
}

接着我们创建一个主页面,在这个页面中我们可以建立融云的连接以及获取会话列表。

connect() { 
    if (this.data.isConnect) {
        this.data.im.disconnect();
    }
    imInstance.connect({ token: config.token }).then(user => {
        this.isConnect = true;
        this.getConversationList();
        console.log('链接成功, 链接用户 id 为: ', user);
    }).catch(error => {
    console.log('链接失败: ', error);
 });
},

注意事项:在此需要注意安全域名问题,要不会报错,导致连接不上融云的服务。

1. 开发环境的话,可以在右上角的详情中的本地设置中将不校验合法域名的地方打上对勾。

2. 生产环境的话,可以参考如下地址设置: https://docs.rongcloud.cn/v4/views/im/noui/guide/private/setting/include/mini.html

这样的话就可以连接成功了,哈哈,第一步总算是跨出去了。

接下来我们可以在连接成功后获取会话列表,代码如下:

imInstance.Conversation.getList().then(list => {
  this.setData({
    conversationList: list
  })
}).catch(error => {
  console.log('获取会话列表失败: ', error);
});

点击详情跳转到聊天页面,在此需要注意如果要获取历史记录的话是要开通im商业版-单群聊云存储的。开通后按照api使用说明获取就可以了,代码如下:

getHistory(conversation) {
    const option = {
        timestrap: +new Date(),
        count: 10
        };
        conversation.getMessages(option).then((result) => { 
            var hasMore = result.hasMore; // 是否还有历史消息可以获取
            this.setData({
              messageList: result.list
            })
        });      
}

接下来发送消息等其他接口,正常调用就可以了,和web端sdk基本是一致的,参考官方文档即可,整个集成过程花了两三天吧,还算顺利。有不足之处还望大家多多指教,多多交流……

融云官网地址:https://www.rongcloud.cn/

融云开发文档地址:https://docs.rongcloud.cn/v4/


集成融云即时通讯碰到的一些问题

IM即时通讯赵炳东 发表了文章 • 0 个评论 • 45 次浏览 • 2021-04-15 15:15 • 来自相关话题

前言公司产品需要新增即时通讯的模块,经过调研后使用融云的即时通讯 SDK 由于以前没有做过相关聊天类的项目,在开发的时候碰到了很多坑,下面会将碰到的问题和解决方案记录下来以做备份和学习交流融云官网:https://www.rongcloud.cn/ ... ...查看全部

前言

公司产品需要新增即时通讯的模块,经过调研后使用融云的即时通讯 SDK 由于以前没有做过相关聊天类的项目,在开发的时候碰到了很多坑,下面会将碰到的问题和解决方案记录下来以做备份和学习交流

融云官网:https://www.rongcloud.cn/ 开发文档:https://docs.rongcloud.cn/v4/

问题列表

1、如何获取历史消息

解决方案: 因为 Web 端没有本地存储,不提供本地获取方法,只能从远端获取 使用获取历史消息方法需要在开发者后台开启 IM 商用版 - 单群聊云存储 服务,服务开通后 30 分钟左右生效 每次最多只能获取 20 条历史消息,通过改变参数 timestrap 来获取其他时间段的历史消息

2、删除会话后还有会话

解决方案: 1、删除会话. 正常来说只要没有收发消息, 会话就不会再生成了 2、如果您有清除 localStorage 的操作, 则 SDK 内部会重新拉取离线消息. 而会话列表是根据收发消息生成的. 则会再次产生会话. 所以建议您检查下是否有清除 localStorage 的操作. 如果有, 建议您只清除自己业务相关的 key 3、如果您换端登录, 则会再次收离线消息, 如果希望此时依然不显示删除的消息. 可以在您删除消息后发一条自定义消息比如: (1). 删除会话 A (2). 发送自定义消息, 标识删除了会话 A (3). 换端登录, 重复收之前已收到过的消息 (4). 收消息时收到了步骤 (2) 中发送的删除标识消息, 根据此自定义消息, 再次调用删除会话方法删除会话

3、怎样才能获取到加入群组之前的聊天记录

解决方案: 加入群组,融云默认是获取不到之前的历史消息,如果想获取到之前的历史消息,需要在开发者后台 -- 免费基础功能 中开通 “新用户获取加入群组前历史消息” 服务,服务开通后 30 分钟左右生效

4、消息中的接收方和发送方 ID 为什么是相同的

解决方案: 如果在接收方来查看消息的话,接收方 id 和 发送方 id 是相同的。 消息体中 targetId 表示会话 id (接收方),senderUserId 表示发送方 id ,messageDirection 表示消息方向,发送: 1,接收: 2 如果 A 给 B 发送一条消息,那么在 B 的角度看的话 targetId 为 A,senderUserId 也是 A,只是 messageDirection 为 2 表示接收 在 A 的角度看的话, targetId 为 B,senderUserId 是 A,只是 messageDirection 为 1 表示发送

5、删除历史消息时,参数与文档描述一致,但是结果却返回 33007 表示:未开通历史消息服务,但是开发者后台早已经开通了

解决方案: 因传入了比融云服务器时间还要大的时间, 所以服务返回了此错误码. 如果要保证删除成功, 建议传入会话最后一条消息的 sentTime (发送时间)

6、接收 emoji 显示不出来

解决方案: 1、Web SDK 接收消息后,消息体内的原生 Emoji 字符会被解码为对应 Unicode 码,需调用转化方法才能正确显示 2、不同浏览器, 不同设备, 展示的原生 Emoji 表情都不同 3、如需多端展示 Emoji 一致, 需使用 emojiToHTML 转化为 HTML 后再展示(此方法为以图片形式展示) 4、emojiToHTML 和 symbolToHTML 仅支持默认的 128 个 emoji. 展示更多, 需自行扩展 详情参考:https://docs.rongcloud.cn/v4/views/im/noui/guide/private/msgmanage/msgsend/web.html#emoji

暂时先记录这些,后续还有的话会继续添加进去,也欢迎大家进行补充,一起学习一下,笔芯 ♥️ ♥️ ♥️ !!!


实现类似微信视频聊天的功能

WebRTC赵炳东 发表了文章 • 0 个评论 • 50 次浏览 • 2021-04-15 15:15 • 来自相关话题

前言今年因为疫情的影响,很多线下的项目或者活动都放在了线上,所以集成音视频等相关的话题一直比较火热。 作为一个小码农对微信音视频通话的功能还是比较好奇的,所以空闲的时候就想着自己能不能也实现一个,给朋友一个链接地址然后就可以愉快的视频通话了(小小的满足一下虚荣... ...查看全部

前言

今年因为疫情的影响,很多线下的项目或者活动都放在了线上,所以集成音视频等相关的话题一直比较火热。 作为一个小码农对微信音视频通话的功能还是比较好奇的,所以空闲的时候就想着自己能不能也实现一个,给朋友一个链接地址然后就可以愉快的视频通话了(小小的满足一下虚荣心)

微信截图_20210415151315.png

对于集成音视频纯小白来说,前期准备工作的确不好整,因为对这块没什么概念,对后面需要做什么列不出一个清晰的步骤出来(蓝廋香菇~~~),幸运的是,在网上找相关知识时意外发现了一家做音视频 SDK 的(融云https://www.rongcloud.cn/

为什么推荐他们家呢,主要是他们的开发文档比较适用于像我这样的小白,跟着他们的快速文档无脑实现音视频通话。点这个地址,开启你的音视频之旅吧:https://docs.rongcloud.cn/v4/views/rtc/call/noui/quick/web.html

特别意外的是融云还提供了 Web 端在线集成体验示例,体验完后还能拿到 demo ,附上跳转连接:https://docs.rongcloud.cn/v4/views/rtc/call/noui/demo/web.html

音视频通话介绍 音视频通话 SDK 含呼叫流程,可以实现一对一呼叫、多人呼叫等音视频通话场景,音视频通话 SDK 区别于音视频会议 SDK,音视频通话(呼叫)SDK 不需要双方约定房间号,呼叫流程自动处理房间号,适用于类似微信音视频通话等必须含有呼叫流程的应用场景,由两部分组成 信令通知 和 音视频传输

信令通知:基于 IMLib SDK,例如 A 给 B 发起通话,A 优先会通过 IM 给 B 发送一个通话请求,B 同意后再进入音视频房间进行音视频通信 音视频传输:基于 RongRTCLib SDK,例如 A 给 B 发起通话,信令传输完成后,会适用融云提供的音视频能力进行通信

但是在集成的时候,有几点是需要吐槽的:

1、我给其他人发起通话的时候,直接就触发挂断了返回 "SummaryMessage" 类型消息,咋又看不懂在文档找了半天都没发现 SummaryMessage 的详细描述,只知道是挂断电话返回的消息,最后提交了工单才了解挂断的原因。

Web 端挂断原因 HungupMessage 消息中 reason 字段及 SummaryMessage 消息中 status 字段都为挂断原因

状态码说明状态码说明
1己方取消已发出的通话请求11对方取消已发出的通话请求
2己方拒绝收到的通话请求12对方拒绝收到的通话请求
3己方挂断13对方挂断
4己方忙碌14对方忙碌
5己方未接听15对方未接听
6己方不支持当前引擎16对方不支持当前引擎
7己方网络错误17对方网络错误
8其他设备已处理18CallLib 不可用

原来是对方忙碌导致的接听失败!!!

2、前段时间还能正常通话,升级完 chrome 后就报错了(一脸懵逼 ),报错信息如下: Failed to execute 'setLocalDescription' on 'RTCPeerConnection': Failed to parse SessionDescription. m=video 9 UDP/TLS/RTP/SAVPF 98 99 96 97 100 101 127 Invalid value:

提供单询问融云的技术人员,才知道是由于 chrome 86 升级了 webRTC 的规范,SDK 在 3.2.6 版本做了相关适配。如果 SDK 使用的不是 3.2.6 版本。需要升级至 3.2.6 版本


融云 IM SDK 集成 —- 刷新会话界面和会话列表界面

IM即时通讯大兴 发表了文章 • 0 个评论 • 678 次浏览 • 2020-12-02 16:28 • 来自相关话题

最近集成融云 IMkit 发现, 融云 IMkit 提供的 ConversationListFragment 和 ConversationFragment 都没有提供刷新方法.我们有个需求是, 使用 Message 的 extra 修改值后, 但是界面没有进行... ...查看全部

最近集成融云 IMkit 发现, 融云 IMkit 提供的 ConversationListFragment 和 ConversationFragment 都没有提供刷新方法.

微信截图_20201202162201.png

我们有个需求是, 使用 Message 的 extra 修改值后, 但是界面没有进行刷新, 需要自己调用代码进行刷新, 但是, 融云没有提供对应的刷新界面的方法, 所以只能看代码.

刷新 ListView 是需要获取 Adapter , 然后使用 Adapter 进行刷新, 但是我们无法从 ConversationListFragment 中直接获取, 那怎么办呢?

我们知道, ListView 中有 getAdapter 的方法. 那我们能不能先获取 ListView 控件呢? 答案是可以的. 我们通过 findById 方法即可获取 ListView. 通过查找, 我们可以知道, 融云会话列表的 listView 的 id 为 R.id.rc_list.

所以代码如下.

ListView mList = (ListView)findViewById(R.id.rc_list);
HeaderViewListAdapter adapter = (HeaderViewListAdapter)mList.getAdapter();
ConversationListAdapter wrappedAdapter = (ConversationListAdapter)adapter.getWrappedAdapter();
if (adapter != null) {
    wrappedAdapter.notifyDataSetChanged();
}


Android 端如何添加自定义表情

IM即时通讯大兴 发表了文章 • 0 个评论 • 567 次浏览 • 2020-12-02 16:28 • 来自相关话题

实现步骤1.新建 RongEmoticonTab 类继承 IEmoticonTab 。public class RongEmoticonTab implements IEmoticonTab {  &... ...查看全部

微信截图_20201202162412.png

实现步骤

1.新建 RongEmoticonTab 类继承 IEmoticonTab 。

public class RongEmoticonTab implements IEmoticonTab {
    public RongEmoticonTab() {
    }
    @Override
    public Drawable obtainTabDrawable(final Context context) {
        return context.getResources().getDrawable(R.drawable.u1f603);
    }
    @Override
    public View obtainTabPager(Context context) {
        return view;
    }
    @Override
    public void onTableSelected(int i) {
    }
}

2.在 obtainTabPager 中添加您想要展示在表情面板上的 view 。

 @Override
    public View obtainTabPager(Context context) {
        View view = LayoutInflater.from(context).inflate(R.layout.view_emoji, null);
        RecyclerView rv = view.findViewById(R.id.recycler_view);
        //LinearLayoutManager是用来做列表布局,也就是单列的列表
        GridLayoutManager mLayoutManager = new GridLayoutManager(context, 5, OrientationHelper.VERTICAL, false);
        rv.setLayoutManager(mLayoutManager);
        //谷歌提供了一个默认的item删除添加的动画
        rv.setItemAnimator(new DefaultItemAnimator());
        rv.setHasFixedSize(true);
        //模拟列表数据
        ArrayList newsList = new ArrayList<>();
        TypedArray array = context.getResources().obtainTypedArray(context.getResources().getIdentifier("rc_emoji_res", "array", context.getPackageName()));
        int i = -1;
        while (++i < array.length()) {
            newsList.add(array.getResourceId(i, -1));
        }
        rv.setAdapter(new NewsAdapter(newsList));
        return view;
    }

3.Adapter 和布局文件可以参考GitHub
4.自定义一个 ExtensionModule 继承自 DefaultExtensionModule,复写其中的 getEmoticonTabs() 方法,返回需要展示的 EmoticonTab 列表。

public class MyExtensionModule extends DefaultExtensionModule {
    private RongEmoticonTab rongEmoticon;
    @Override
    public List<IEmoticonTab> getEmoticonTabs() {
        List<IEmoticonTab> emoticonTabs =  super.getEmoticonTabs();
        RongEmoticonTab emojiTab=new RongEmoticonTab();
        emoticonTabs.add(myEmoticon);
        return emoticonTabs;
    }
}

5.在初始化之后,取消 SDK 默认的 ExtensionModule,注册自定义的 ExtensionModule, 如下:

public void setMyExtensionModule() {
    List<IExtensionModule> moduleList = RongExtensionManager.getInstance().getExtensionModules();
    IExtensionModule defaultModule = null;
    if (moduleList != null) {
        for (IExtensionModule module : moduleList) {
            if (module instanceof DefaultExtensionModule) {
                defaultModule = module;
                break;
            }
        }
        if (defaultModule != null) {
            RongExtensionManager.getInstance().unregisterExtensionModule(defaultModule);
            RongExtensionManager.getInstance().registerExtensionModule(new MyExtensionModule());
        }
    }
}

6.如果需要网络下载表情需要下载并持久化表情数据,需要在添加 EmoticonTab 前下载好表情数据。

融云即时通讯SDK集成 — 通知检查

IM即时通讯大兴 发表了文章 • 2 个评论 • 658 次浏览 • 2020-12-02 16:28 • 来自相关话题

背景:最近公司新上的app要加上即时通讯的功能, 自己快速实现一个当然是不可能的了(项目deadline也顶不住哇).就从各家成熟的SDK厂商选来选去的, 各有各的好也各有各的不足.最后点兵点将,选了融云家的SDK(老板说了算hhhh).他家的官网和文档地址:... ...查看全部

微信截图_20201202162632.png背景:

最近公司新上的app要加上即时通讯的功能, 自己快速实现一个当然是不可能的了(项目deadline也顶不住哇).就从各家成熟的SDK厂商选来选去的, 各有各的好也各有各的不足.最后点兵点将,选了融云家的SDK(老板说了算hhhh).
他家的官网和文档地址:
官网:https://www.rongcloud.cn/
文档:https://docs.rongcloud.cn/v4
这个任务当然还是落在我的头上. 集成完毕后, 也踩了不少坑. 所以这篇文章给大家总结下排查融云消息的本地通知和远端推送的办法. 希望可以帮助到正在看这篇文章的你.

什么是本地通知:

当我的App接入了融云的即时通讯sdk后, 便拥有了即时通讯的能力. sdk与融云服务器建立长连接, 当消息发出后, 先走到融云的服务器, 再转发给相应的用户. 这里移动端到服务端, 服务端到移动端, 走的通道都是长连接. 无论你的app是在前台还是在后台, 只要没有被杀死, 那么长连接是一直在的. 所以消息可以即时的发送到达给接收者. 融云把这种走长连接到达的消息, 在通知栏展示的通知叫做本地通知. 也就是消息是顺利发送到接收者端了, 逻辑可以走到消息接收监听那里. 融云sdk内部实现了消息到达后的本地通知, 也赋予了开发者自行实现消息到达后进行本地通知的权利.

本地通知的检查:

这里我总结了一下接入融云sdk后, 关于本地通知接收不顺利的排查. 大致可以分为这么几条:

1.是否设置了 setOnReceiveMessageListener 监听, 并且 onReceived 方法返回的为 true。

 RongIM.setOnReceiveMessageListener(new RongIMClient.OnReceiveMessageListener() {
              @Override
              public boolean onReceived(Message message, 
                  return true;
              }
          });

如果 onReceived 方法返回值为 true 则是监听做了拦截, 则不会走通知逻辑。 2.发送的消息是否是自定义消息。 如果是自定义消息, 则请查看自定义消息的 MessageTag 的注解是否设置了 flag 的值为 MessageTag.ISCOUNTED 或 MessageTag.ISPERSISTED) 。 以下面为例。

@MessageTag(value = "RC:TxtMsg", flag = MessageTag.ISCOUNTED)
  @DestructionTag
  public class TextMessage extends MessageContent {
      ...
  }

如果没有设置其中之一,则不会走通知逻辑。

3.是否设置消息拦截器

设置代码如下:

/**
   * 设置接收消息时的拦截器
   *
   * @param messageInterceptor 拦截器
   */
  public void setMessageInterceptor(new RongIM.getInstance().MessageInterceptor() {
          @Override
          public boolean intercept(final Message message) {
                 return true;
          }
 });

如果设置了消息拦截器, 并且在 intercept() 方法中返回 true, 表示拦截此消息。 则不会走通知逻辑。

4.检查接收到的消息的 senderUserId 是否和当前用户的 id 相同。
如果 id 相同, 则不会走通知逻辑。 自己不可发给自己。

5.查看当前接收的消息是否是属于聊天室会话类型的
聊天室的消息目前不支持本地通知。

6.如果是自定义消息,请检查是否设置了自定义消息的 MessageProvider 。
如果没有,则不会走通知逻辑。

7.接收方本地是否有接收方的用户信息。 如果没有设置, 则不会走通知逻辑。

8.检查应用权限设置,看是否打开应用信任权限, 通知栏权限或者声音提示权限等。如果没有打开,请打开再试。

9.接收方没有发送方的用户信息,接收端不弹通知。用户信息通过用户信息提供者方式获取。
参考链接:https://docs.rongcloud.cn/v3/views/im/ui/guide/private/user/set/android.html

什么是远端推送:

集成了即时通讯的SDK, 我们的app不就能拥有像微信一样随时随地收到消息的即时通讯能力了? 说实话我一开始也是这么认为的. 可惜做开发也要按基本法来, Android平台回收app的这一关咱都过不了, app都给你杀死得透透的了你拿啥收消息呢? 咱又不是微信hhhh. 所以一番急赤白脸地阅读他家文档之后, 才发现app如果活着, 他融云能用自己的通道给你把消息推送到. 如果app被杀死了, 这个消息就在他家的服务端直接交给三方厂商了(也就是五大厂商蓝绿大厂华为小米FCM), 让这条消息走人家厂商的推送通道给送到你手机上.

远端推送的检查:

这里我总结了一下接入融云sdk后, 关于本地通知接收不顺利的排查. 大致可以分为这么几条:

  1. 退出应用的时候,只能调用融云的 disconnect() 方法,而不是 logout()。这样退出后融云才会启动push进程。

  2. push 进程的名字不能更改,必须是默认的名字,既 io.rong.push.
    (2.6.0dev之后的版本,此进程名字可以修改)

  3. 通过 ddms 或者终端里敲入 adb shell ps|grep rong 查看终端里是否存在 io.rong.push 这个进程。

有些手机厂家做了特殊限制,不允许第三方后台进程启动,所以融云的后台进程起不来,导致收不到 push 消息。这种情况可以换个手机测试(如三星,大部分三星手机没有做权限限制)。另外大部分国产手机,有权限设置的菜单,比如小米,华为等,可以手动去安全中心,设置应用的自启动权限,后台运行权限等,就可以收到Push消息了。

不过 vivo 和 oppo 有些型号的手机,一旦应用退到后台,系统会很快把它杀死,这种没有办法解决。目前市面上所有推送都存在这个问题,除非系统把该应用加入白名单。

  1. push 进程存在,仍然收不到 push 消息。

如果你的应用有消息免打扰功能,那么请确认当前登录账号之前是否设置过消息免打扰。如果不太确定,那最好去你的应用设定里重新设置下消息免打扰时间。 这里要注意的是: 如果这个账号之前在别的手机上设置过消息免打扰,换一台手机登录或者卸载重装的时候,融云服务端记录的仍然是之前设置的消息免打扰状态,所以这种情况下是收不到push消息的。


解决融云 SDK 4.0 版本配置 https 导航报 SSLHandshakeException

IM即时通讯大兴 发表了文章 • 0 个评论 • 575 次浏览 • 2020-12-02 16:28 • 来自相关话题

我们公司最近使用融云 IM 进行集成开发. 我们是私有云部署, 导航是通过接口进行设置的. 是 Https 的.之前我们是使用的2.x 版本, 使用下面接口配置一下即可.    RongIMClient.getInsta... ...查看全部

微信截图_20201202162800.png

我们公司最近使用融云 IM 进行集成开发. 我们是私有云部署, 导航是通过接口进行设置的. 是 Https 的.

之前我们是使用的2.x 版本, 使用下面接口配置一下即可.

    RongIMClient.getInstance().enableHttpsSelfCertificate(false);

但是最近我们升级融云到 4.0 版本. 突然发现连接不上了. 日志中报

javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.

这个错误.

于是咨询融云技术支持, 得知 4.0 需要自己进行证书验证.

需在 application 中添加即可.

 try {
    //使用X509TrustManager代替CertificateTrustManager跳过证书验证。
    TrustManager tm[] =  new X509TrustManager() {
                @Override
                public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                }
                @Override
                public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                }
                @Override
                public X509Certificate[] getAcceptedIssuers() {
                    return new X509Certificate[0];
                }
            }
        };
        SSLContext context = SSLContext.getInstance("TLS");   
        context.init(null, tm, null);
        SSLUtils.setSSLContext(context);
        SSLUtils.setHostnameVerifier(new HostnameVerifier() {
            @Override
            public boolean verify(String hostname, SSLSession session) {
                return true;
             }
            });
} catch (Throwable e) {
    throw new IllegalStateException(e);
}

注意的是, 由于导航请求是在融云的 ipc 进程中的, 所以在 application 中设置上面代码的时候, 千万不要设置在主进程中. 否则无效.

在融云 IMkit 会话界面基础上添加消息已读未读.

WebRTC大兴 发表了文章 • 0 个评论 • 714 次浏览 • 2020-11-06 15:37 • 来自相关话题

使用过融云的同学们可能知道. 融云 IMkit 的会话界面,  发送玩消息后, 如果对方已读, 发送端则会显示小对号的图片. 但是更具需求要把小对号改为已读未读. 接下来我们就一块实现这个功能.打开回执功能首先, 要确定打开融云的消息回执功能. 这个... ...查看全部

使用过融云的同学们可能知道. 融云 IMkit 的会话界面,  发送玩消息后, 如果对方已读, 发送端则会显示小对号的图片. 但是更具需求要把小对号改为已读未读. 接下来我们就一块实现这个功能.

打开回执功能

首先, 要确定打开融云的消息回执功能. 这个很简单, 就是在 rc_config.xml 中把下面属性配置为 true 即可.

<!-- 设置是否开启消息回执, true 为打开, false 为关闭--><bool name="rc_read_receipt">true</bool>

这样发送消息后, 对方已读, 发送端就会出现融云默认的小对号了.

自定义 Adapter

出现小对号后, 下一步就是要进行对融云适配器的改造了.

第一步
创建 CustomMessageListAdapter 继承 MessageListAdapter. 根据自己的需求复写 newView() 或者 bindView() 方法.

class CustomMessageListAdapter extends MessageListAdapter {    @Override    protected void bindView(View v, final int position, final UIMessage data) {        // 此方法中操作控件    }}

第二步

创建 CustomConversationFragment 继承于 ConversationFragment, 并复写父类中的 onResolveAdapter(), 返回 CustomMessageListAdapter 对象.

 class CustomMessageListAdapter extends ConversationFragment {        @Override        public MessageListAdapter onResolveAdapter() {            return new CustomMessageListAdapter();        }    }

第三步

使用 CustomConversationListFragment 代替 ConversationListFragment 进行配置使用即可.

u=2322698630,4022941591&fm=26&gp=0.jpg

添加已读未读

自定了了 Adapter, 我们就可以在原先逻辑的基础上进行扩展了. 现在主要的任务就是找到小对号的控件.

我们先看一下默认的 item 布局 rc_item_message.xml. 在布局中我们可找到下面的控件

    <TextView        android:id="@id/rc_read_receipt"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_gravity="bottom"        android:layout_marginRight="4dp"        android:drawableLeft="@drawable/rc_read_receipt"        android:drawableStart="@drawable/rc_read_receipt"        android:textColor="@color/rc_read_receipt_status"        android:textSize="12sp"        android:visibility="gone" />

这个就是显示对号的控件了.  是一个 TextView 的控件, 由于我们是添加 “已读” 、“未读” 字符串, 正好可以直接使用这个 TextView 控件.

我们通过控件的 id 在 Adapter 中找到控件的名称.

class CustomMessageListAdapter extends MessageListAdapter {    @Override    protected void bindView(View v, final int position, final UIMessage data) {         super(v, position, data);       // 此方法中操作控件       final ViewHolder holder = (ViewHolder) v.getTag();       if (data.getMessageDirection() == Message.MessageDirection.SEND) {            if (readRec && data.getSentStatus() == Message.SentStatus.READ) {                if (data.getConversationType().equals(Conversation.ConversationType.PRIVATE) && tag.showReadState()) {                    holder.readReceipt.setVisibility(View.VISIBLE);                    holder.readReceipt.setText(已读);                    holder.readReceipt.setBackground(null);                } else {                      holder.readReceipt.setText(未读);                      holder.readReceipt.setBackground(null);                      holder.readReceipt.setVisibility(View.VISIBLE);            }        }    }}

这样就会显示就会显示已读未读的字样了.


融云集成之避坑指南-Android推送篇

WebRTC大兴 发表了文章 • 0 个评论 • 657 次浏览 • 2020-11-06 15:37 • 来自相关话题

在集成推送时候,需要先了解融云针对推送的定义: Push 通知是当接收方的主进程被杀死或者回收,或者您主动调用 disconnect() 的操作, 导致 IM 长连接通道与服务器断开后。 在这种情况下,会收到Push 服务;说白了,就是应用进程被杀掉,才会收到... ...查看全部

在集成推送时候,需要先了解融云针对推送的定义: Push 通知是当接收方的主进程被杀死或者回收,或者您主动调用 disconnect() 的操作, 导致 IM 长连接通道与服务器断开后。 在这种情况下,会收到Push 服务;

说白了,就是应用进程被杀掉,才会收到push 的,否则的话,是会走融云内部的长链接通道的;若是连应用进程都不了解的话,我要拿起我四十米的大刀了。
avatar

好了,前提介绍结束,接下来,我们正式开始避坑。

融云的推送分为俩大类:RongPush 以及其他厂商推送;至于为什么会有厂商推送 ,主要书由于众说周知的原因,各个手机厂商针对系统的room 太深,导致融云的push 进程服务无法自启动以及无法常存与服务后台,所以为了满足离线时候,push 的到达率,所以接入了主流的厂商推送服务,至于你说,为什么不接入统一推送联盟,估计等推送联盟孵化出来,可能得等到2045年了;哈哈,估计我再不开始介绍接入,大家就要发飙了。
avatar

各位客官,稍安勿躁,马上开始。本篇我们先介绍厂商推送之华为推送过程中遇到的坑;

首先,按照融云官网的描述,融云SDK 目前是有俩个版本,2.x 版本的SDK 以及4.x 版本的SDK ,分别对应的是华为的2.x 的jar 以及4.x 的jar ,可以分别参考 2.x版本 以及4.x版本,按照文档进行集成即可,具体集成过程按照文档集成即可,此处无需多说;

接下来,先介绍我在集成2.x 版本时候遇到的问题:

1.出现的问题一,没有填写对应的应用标识;(此处是我的锅,没有细致的看文档);

按照融云文档的描述,
avatar 此处要填写对应的华为的appkey 等数据的,而愚蠢的我竟然没有填写对应的 AppIDAppSecret,希望各位 大大在集成过程中不要跟我一样犯傻;

2.出现的问题二,按照文档配置完成了,结果,还是收不到推送,不会进行排查;

似乎这个问题,很多小伙伴都遇到了,所以,我在融云知识库中找到了一篇官方提供的排查手段,仅供大家参考 https://support.rongcloud.cn/ks/ODg0 ,若是大家还是搞不定,可以直接找融云进行技术支持的;

以上是在集成2.x 版本时候遇到的问题,不算特别困难,也是比较轻松的解决了;

但是,事情万万没有想到的那么简单,时间过去一个月,项目要升级4.x 的融云SDK ,于是,华为推送也要进行对应的升级,于是我开开心心准备认真参谋一下4.x文档,还好,文档比较简单;

于是,对应的问题,也出现了:

问题1:在升级过程中,并没有找见 agconnect-servics.json  文件的下载地址。

经过与融云技术人员的沟通,在华为提供5.0版本的推送服务之后,agconnect-servics.json  下载位置改变了,所以我将我知道的下载路径提供给大家,在华为开发者平台-我的项目中-对应的项目下载即可。

问题2: 由于我是从2.x 升级到4.x 的,所以,眼瞎的我没有看清楚文档中AndroidMainfest 中的配置是要删除的,所以导致找不到文件了,所以请大家一定要看清楚标题,是删除配置,不是添加配置;

问题3: 在EMUI 10以上 是可以收到推送的,但是在EMUI 10以下,收不到推送。

这个问题经过与融云技术人员以及华为技术人员的沟通可以确定,由于华为不再继续维护2.0推送服务,改推4.0 以上的服务,但是4.0的推送服务,又对HMSCore 是由要求的,所以只能升级用户的 HMSCore服务,不过融云对外提供了升级监听,具体可以参考融云文档


iOS 性能优化:优化 App 启动速度

IM即时通讯大神庵 发表了文章 • 0 个评论 • 203 次浏览 • 2020-10-26 12:00 • 来自相关话题

作者:Damonwong,iOS 开发者来源公众号丨老司机技术周报(ID:LSJCoding)Sessions: https://developer.apple.com/videos/play/wwdc2019/423/苹果是一家特别注重用户体验的公司,过去几... ...查看全部

作者:Damonwong,iOS 开发者

来源公众号丨老司机技术周报(ID:LSJCoding)

Sessions: https://developer.apple.com/videos/play/wwdc2019/423/

苹果是一家特别注重用户体验的公司,过去几年一直在优化 App 的启动时间,特别是去年的 WWDC 2019 keynote[1] 上提到,在过去一年苹果开发团队对启动时间提升了 200%

虽然说是提升了 200%,但是有些问题还是没有说清楚,比如:

  • 为什么优化了这么多时间?

  • 作为开发者的我们,我们还可以做哪些针对启动速度的优化?

所以我们今天结合 WWDC2019 - 423 - Optimizing App Launch[2] 聊一下和启动相关的东西

名词解释

先介绍一些和启动相关的名词。

Mach-O

Mach-O 是 iOS 系统不同运行时期可执行的文件的文件类型统称。主要分以下三类:

  • Executable - 可执行文件,是 App 中的主要二进制文件

  • Dylib - 动态库,在其他平台也叫 DSO 或者 DLL

  • Bundle - 苹果平台特有的类型,是无法被连接的 Dylib。只能在运行时通过 dlopen() 加载

Mach-O 的基本结构如下图所示,分为三个部分:

  • Header 包含了 Mach-O 文件的基本信息,如 CPU 架构,文件类型,加载指令数量等

  • Load Commands 是跟在 Header 后面的加载命令区,包含文件的组织架构和在虚拟内存中的布局方式,在调用的时候知道如何设置和加载二进制数据

  • Data 包含 Load Commands 中需要的各个 Segment 的数据。

绝大多数 Mach-O 文件包括以下三种 Segment

  • __TEXT - 代码段,包括头文件、代码和常量。只读不可修改。

  • __DATA - 数据段,包括全局变量, 静态变量等。可读可写。

  • __LINKEDIT - 如何加载程序, 包含了方法和变量的元数据(位置,偏移量),以及代码签名等信息。只读不可修改。

Image

指的是 Executable,Dylib 或者 Bundle 的一种。

Framework

有很多东西都叫做 Framework,但在本文中,Framework 指的是一个 dylib,它周围有一个特殊的目录结构来保存该 dylib 所需的文件。

虚拟内存(Virtual Memory)

虚拟内存是建立在物理内存和进程之间的中间层。是一个连续的逻辑地址空间,而且逻辑地址可以没有对应的实际物理内存地址,也可以让多个逻辑地址对应到一个物理内存地址上。

Page Fault

当进程访问一个没有对应物理地址的逻辑地址时,会发生 Page Fault

Lazy Reading

某个想要读取的页没有在内存中就会触发 Page Fault,系统通过调用 mmap() 函数读取指定页,这个过程叫做 Lazy Reading

COW(Copy-On-Write)

当进程需要对某一页内容进行修改时,内核会把需要修改的部分先复制一份,然后再修改,并把逻辑地址重新映射到新的物理内存去。这个过程叫做 Copy-On-Write

Dirty Page & Clean Page

Image 加载后,被修改过内容的 Page 叫做 Dirty Page,会包含着进程特定的信息。与之相对的叫 Clean Page,可以从磁盘重新生成。

共享内存(Share RAM)

当多个 Mach-O 都依赖同一个 Dylib(eg. UIKit)时,系统会让这几个 Mach-O 的调用 Dylib 的逻辑地址都指向同一块物理内存区域,从而实现内存共享。Dirty Page 为进程独有,不能被共享。

地址空间布局随机化(ASLR)

当 Image 加载到逻辑地址空间的时候,系统会利用 ASLR 技术,使得 Image 的起始地址总是随机的,以避免黑客通过起始地址+偏移量找到函数的地址

当系统利用 ASLR 分配了随机地址后,从 0 到该地址的整个区间会被标记为不可访问,意味着不可读,不可写,不可被执行。这个区域就是 __PAGEZERO 段,它的大小在 32 位系统是 4KB+,而在 64 位系统是 4GB+

代码签名(Code Sign)

代码签名可以让 iOS 系统确保要被加载的 Image 的安全性,用 Code Sign 设置签名时,每页内容都会生成一个单独的加密散列值,并存储到 __LINKEDIT 中去,系统在加载时会校验每页内容确保没有被篡改。

dyld(dynamic loader)

dyld 是 iOS 上的二进制加载器,用于加载 Image。有不少人认为 dyld 只负责加载应用依赖的所有动态链接库,这个理解是错误的。dyld 工作的具体流程如下:参考:dyld启动流程[3]

Load dylibs

dyld 在加载 Mach-O 之前会先解析 Header 和 Load Commands, 然后就知道了这个 Mach-O 所依赖的 dylibs,以此类推,通过递归的方式把全部需要的 dylib 都加载进来。

一般来说,一个 App 所依赖的 dylib 在 100 - 400 左右,其中大多数都是系统的 dylib,因为有缓存和共享的缘故,读取速度比较高。

Fix-ups

因为 ASLR 和 Code Sign 的原因,刚被加载进来的 dylib 都处于相对独立的状态,为了把它们绑定起来,需要经过一个 Fix-ups 过程。Fix-ups 主要有两种类型:Rebase 和 Bind。

PIC(Position Independent Code)

因为代码签名的原因,dyld 无法直接修改指令,但是为了实现在运行时可以 Fix-ups,在 code gen 时,通过动态 PIC(Position Independent Code)技术,使本来因为代码签名限制不能再修改的代码,可以被加载到间接地址上。当要调用一个方法时,会先在 __DATA 段中建立一个指针指向这个方法,再通过这个指针实现间接调用。

Rebase

Rebase 就是针对“因为 ASLR 导致 Mach-O 在加载到内存中是一个随机的首地址”这一个问题做一个数据修正的过程。会将内部指针地址都加上一个偏移量,偏移量的计算方法如下:

  Slide = actual_address - preferred_address

所有需要 Rebase 的指针信息已经被编码到 __LINKEDIT 里。然后就是不断重复地对 __DATA 中需要 Rebase 的指针加上这个偏移量。这个过程中可能会不断发生 Page Fault 和 COW,从而导致 I/0 的性能损耗问题,不过因为 Rebase 处理的是连续地址,所以内核会预先读取数据,减少 I/O 的消耗。

Binding

Binding 就是对调用的外部符号进行绑定的过程。比如我们要使用到 UITableView,即符号 _OBJC_CLASS_$_UITableView, 但这个符号又不在 Mach-O 中,需要从 UIKit.framework 中获取,因此需要通过 Binding 把这个对应关系绑定到一起。

在运行时,dyld 需要找到符号名对应的实现。而这需要很多计算,包括去符号表里找。找到后就会将对应的值记录到 __DATA 的那个指针里。Binding 的计算量虽然比 Rebasing 更多,但实际需要的 I/O 操作很少,因为之前 Rebasing 已经做过了。

dyld2 & dyld3

 

在 iOS 13 之前,所有的第三方 App 都是通过 dyld 2 来启动 App 的,主要过程如下:

  • 解析 Mach-O 的 Header 和 Load Commands,找到其依赖的库,并递归找到所有依赖的库

  • 加载 Mach-O 文件

  • 进行符号查找

  • 绑定和变基

  • 运行初始化程序

上面的所有过程都发生在 App 启动时,包含了大量的计算和I/O,所以苹果开发团队为了加快启动速度,在 WWDC2017 - 413 - App Startup Time: Past, Present, and Future[4] 上正式提出了 dyld3。

dyld3 被分为了三个组件:

  • 一个进程外的 MachO 解析器


    • 预先处理了所有可能影响启动速度的 search path、@rpaths 和环境变量

    • 然后分析 Mach-O 的 Header 和依赖,并完成了所有符号查找的工作

    • 最后将这些结果创建成了一个启动闭包

    • 这是一个普通的 daemon 进程,可以使用通常的测试架构

  • 一个进程内的引擎,用来运行启动闭包


    • 这部分在进程中处理

    • 验证启动闭包的安全性,然后映射到 dylib 之中,再跳转到 main 函数

    • 不需要解析 Mach-O 的 Header 和依赖,也不需要符号查找。

  • 一个启动闭包缓存服务


    • 系统 App 的启动闭包被构建在一个 Shared Cache 中, 我们甚至不需要打开一个单独的文件

    • 对于第三方的 App,我们会在 App 安装或者升级的时候构建这个启动闭包。

    • 在 iOS、tvOS、watchOS中,这这一切都是 App 启动之前完成的。在 macOS 上,由于有 Side Load App,进程内引擎会在首次启动的时候启动一个 daemon 进程,之后就可以使用启动闭包启动了。

dyld 3 把很多耗时的查找、计算和 I/O 的事前都预先处理好了,这使得启动速度有了很大的提升。

App 启动

介绍完上面这一大堆名词之后,我们正式进入主题。

App 启动为什么这么重要?

  • App 启动是和用户的第一个交互过程,所以要尽量缩短这个过程的时间,给用户一个良好的第一印象

  • 启动代表了你的代码的整体性能,如果启动的性能不好,其他部分的性能可能也不会太好

  • 启动会占用 CPU 和内存,从而影响系统性能和电池

所以我们要好好优化启动时间。

启动类型

App 的启动类型分为三类

  • Cold Launch 也就是冷启动,冷启动需要满足以下几个条件:


    • 重启之后

    • App 不在内存中

    • 没有相关的进程存在

  • Warm Launch 也就是热启动,热启动需要满足以下几个条件:


    • App 刚被终止

    • App 还没完全从内存中移除

    • 没有相关的进程存在

  • Resume Launch 指的是被挂起的 App 继续的过程,需要满足以下几个条件:


    • App 被挂起

    • App 还全部都在内存中

    • 还存在相关的进程

App 启动阶段

App 启动分为三个阶段

  • 初始化 App 的准备工作

  • 绘制第一帧 App 的准备工作及绘制(这里的第一帧并不是获取到数据之后的第一帧,可以是一张占位视图),这时候用户与App已经可以交互了,比如 tabbar 切换

  • 获取到页面的所有数据之后的完整的绘制第一帧页面

在这个地方,苹果再次强调了一下,建议「用户从点击 App 图标到可以再次交互,也就是第二阶段结束」的时间最好在 400ms 以内。目前来看,大部分 App 都没有达到这个目标。

下面,我们把上面三个阶段分成下面这 6 个部分,讲一下这几个阶段做了什么以及有什么可以优化的地方。

 

System Interface

初始化 App 的准备工作,系统主要做了两个事情:Load dylibs 和 libSystem init

在 2017 年苹果介绍过 dyld3 给系统 App 带来了多少优化,今年 dyld3 正式开发给开发者使用,这意味着 iOS 系统会将你热启动的运行时依赖给缓存起来。以达到减少启动时间的目的。这也就是提升 200% 的原因之一。

视频中只说优化了热启动时间,理论上对于 iOS 系统来说 dyld3 应该还可以优化冷启动时间,所以不知道是因为给 iPad 增加了多任务功能的原因,还是没有把所有功能开放的原因,作者只提了热启动这个原因暂时还不太清楚。

除此之外,在 Load dylibs 阶段,开发者还可以做以下优化:

  • 避免链接无用的 frameworks,在 Xcode 中检查一下项目中的「Linked Frameworks and Librares」部分是否有无用的链接。

  • 避免在启动时加载动态库,将项目的 Pods 以静态编译的方式打包,尤其是 Swift 项目,这地方的时间损耗是很大的。

  • 硬链接你的依赖项,这里做了缓存优化。

也许有人会困惑是不是使用了 dyld3 了,我们就不需要做 Static Link 了,其实还是需要的,感兴趣的可以看一下 Static linking vs dyld3[5] 这篇文章,里面有一个详细的数据对比。

libSystem init 部分,主要是加载一些优先级比较低的系统组件,这部分时间是一个固定的成本,所以我们开发人员不需要关心。

Static Runtime Initializaiton

这个阶段主要是 Objective-C 和 Swift Runtime 的初始化,会调用所有的 +load 方法,将类的信息注册到 runtime 中。

在这个阶段,原则上不建议开发者做任何事情,所以为了避免一些启动时间的损耗,你可以做以下几个事情:

  • 在 framework 开发时,公开专有的初始化 API

  • 减少在 +load 中做的事情

  • 使用 initialize 进行懒加载初始化工作

UIKit Initializaiton

这个阶段主要做了两个事情:

  • 实例化 UIApplication 和 UIApplicationDelegate

  • 开始事件处理和系统集成

所以这个阶段的优化也比较简单,你需要做两个事情:

  • 最大限度的减少 UIApplication 子类初始化时候的工作,更甚至与不子类化 UIApplication

  • 减少 UIApplicationDelegate 的初始化工作

Application Initializaiton

这个阶段主要是生命周期方法的回调,也正是开发者最熟悉的部分。

调用 UIApplicationDelegate 的 App 生命周期方法:

  application:willFinishLaunchingWithOptions: 
  application:didFinishLaunchingWithOptions:

和 UIApplicationDelegate 的 UI 生命周期方法:

  applicationDidBecomeActive:

同时,iOS 13 针对 UISceneDelegate 增加了新的回调:

  scene:willConnectToSession:options:
  sceneWillEnterForeground:
  sceneDidBecomeActive:

也会在这个阶段调用。感兴趣的可以关注一下 Getting the Most out of Multitasking 这个 Session,暂时没有视频资源,怀疑是现场演示翻车了,所以没有把视频资源放出来。

在这个阶段,开发者可以做的优化:

  • 推迟和启动时无关的工作

  • Senens 之间共享资源

Fisrt Frame Render

这个阶段主要做了创建、布局和绘制视图的工作,并把准备好的第一帧提交给渲染层渲染。会频繁调用以下几个函数:

 loadView
 viewDidLoad 
 layoutSubviews

在这个阶段,开发者可以做的优化:

  • 减少视图层级,懒加载一些不需要的视图

  • 优化布局,减少约束

更多细节可以从 WWDC2018 - 220 - High Performance Auto Layout[6] 中了解

Extend

大部分 App 都会通过异步的方式获取数据,并最终呈现给用户。我们把这一部分称为 Extend。

因为这一部分每个 App 的表现都不一样,所以苹果建议开发者使用 os_signpost 进行测量然后慢慢分析慢慢优化。

测量 App 启动时间

要找到启动过程中的问题,就要进行多次测量并前后比较。但是如果变量没有控制好,就会导致误差。

所以为了保证测量的数据能够真实的反应问题,我们要减少不稳定性因素,保证在可控的相近的环境下进行测量。最后使用一致的结果来分析。

条件一致性

为了保证环境一致,我们可以做下面这几个事情:

  • 重启手机,并等待 2-3 分钟

  • 启用飞行模式或者使用模拟网络

  • 不使用或者不变更 iCloud 的账户

  • 使用 release 模式进行 build

  • 测量热启动时间

iColud 账户切换会影响性能,所以不要切换账号或者不开启 iCloud。

测量注意点

  • 尽可能的使用具有代表性的数据进行测试

    如果不使用具有代表性的数据进行测试,就会出现偏差

  • 使用不同的新旧设备进行测试

  • 最后你还可以使用 XCTest 来测试,多运行几次,取平均结果

关于使用 XCTest 测试启动时间的信息,可以看一下 WWDC2019 - 417 - Improving Battery Life and Performance[7],但是我测试了一下,目前好像还有一部分 API 还没有开放出来,暂时还不能使用。

使用 Instruments 分析和优化 App 启动过程

优化方式

苹果给了我们三个优化方式的建议,整体思想和前面提到的各个阶段的优化差不多

Minimize Work

  • 推迟与第一帧无关的工作

  • 从主线程移开阻塞工作

  • 减少内存使用量

Prioritize Work

  • 定义好任务的优先级。

  • 利用好 GCD 来优化你的启动速度。

  • 让重要的事情保持优先

更深入的了解有关 GCD 的内容,可以看一下 WWDC2017 - 706 - Modernizing Grand Central Dispatch Usage[8]

Optimize Work

  • 简化现有工作,比如只请求必要的数据。

  • 优化算法和数据结构

  • 缓存资源和计算

使用 Instruments 分析 App 启动过程

当知道如何优化之后,我们需要针对我们的启动过程进行分析。Xcode 11 的 Instruments 为此新增了一个 App launch 模板,让开发者可以更好的分析自己 App 的启动速度。

 

运行后可以看到各个阶段的具体时间,根据数据进行优化,还能看到耗时的函数调用。

 

系统优化

去年苹果做了很多优化,下面这几个高亮的是和启动速度有关的优化

 

但是不知道是不是时间原因,在 session 中对于这部分的解释特别少,很难理解 200% 到底做了什么。

但是 Craig Federighi 在 The Talk Show Live From WWDC 2019, With Craig Federighi and Greg Joswiak[9] 中针对为什么优化了 200% 说了这样一段话:

Isn’t that crazy that was quite a discovery for us. No it turns out that over times as in terms of the way the apps were encrypted and the way fair play worked and so forth. The encryption became part of the critical path actually of launching the apps. I mean the processors are capable or up and through the thing that actually it was a problem. And then there are other optimizations that based on what was visible to system at certain things. And so it actually cut out optimization opportunities and so when we really identified that opportunity we said okay. We can actually come up with better format that’s gonna eliminate that being on the critical path, It’s going to enable all these pre-binding things. And then we did a whole bunch of other work to optimize the objective-c runtime to optimize the linker the dynamic linker a bunch of other things and you put it all together. And yeah that I mean a cold launch this is we’ve never had a win like this to launch time in a single release.

从这段话中,除了 dyld3 的功劳之外,减少对代码签名加密也是优化之一。

监控线上用户 App 的启动

Xcode 11 在 Xcode Organizer 新增了一个监控面板,在这个面板里面可以查看多个维度的用户数据,其中还包括平均启动时间。

 

当你通过 Instruments 分析完你的启动过程,并做了大量优化之后,你就可以通过 Xcode Organizer 来分析你这次优化效果到底怎么样。

当然你可以通过去年新出的 MetricKit[10] 获取一些自定义的数据,具体参照 WWDC2019 - 417 -Improving Battery Life and Performance[11]

总结

今年苹果提的很多优化建议其实在早几年都已经说过一遍了,属于老生常谈了。但是值得点赞的是新出的 Instruments 的 App launch 和 Xcode Organizer 的性能监控。

启动优化是一个需要经常反复做的事情,越早发现问题,越容易解决问题,如果你想给你的用户一个良好的第一印象,就赶快行动起来吧。

参考资料

[1]

WWDC 2019 keynote: https://developer.apple.com/videos/play/wwdc2019/101/

[2]

WWDC2019 - 423 - Optimizing App Launch: https://developer.apple.com/videos/play/wwdc2019/423/

[3]

dyld启动流程: https://leylfl.github.io/2018/05/28/dyld启动流程/

[4]

WWDC2017 - 413 - App Startup Time: Past, Present, and Future: https://developer.apple.com/videos/play/wwdc2017/413/

[5]

Static linking vs dyld3: https://allegro.tech/2018/05/Static-linking-vs-dyld3.html

[6]

WWDC2018 - 220 - High Performance Auto Layout: https://developer.apple.com/videos/play/wwdc2018/220/

[7]

WWDC2019 - 417 - Improving Battery Life and Performance: https://developer.apple.com/videos/play/wwdc2019/417/

[8]

WWDC2017 - 706 - Modernizing Grand Central Dispatch Usage: https://developer.apple.com/videos/play/wwdc2017/706/

[9]

The Talk Show Live From WWDC 2019, With Craig Federighi and Greg Joswiak: https://daringfireball.net/2019/06/the_talk_show_live_from_wwdc_2019

[10]

MetricKit: https://developer.apple.com/documentation/metrickit

[11]

WWDC2019 - 417 -Improving Battery Life and Performance: https://developer.apple.com/videos/play/wwdc2019/417/


APP