一. 即时通讯技术方案

1. 第三方SDK: 环信, 融云, 网易云信, 腾讯

  1. 中小型公司/初创型: 建议使用第三方.
  2. 好处: 快, 符合快速开发的需求, 自己和后台人员不需要做什么操作
  3. 缺点: 你的数据会经过人家的服务器, 可能会不安全

2. 使用XMPP: XMPPFramework, 以前做即时通讯, 基本都在使用XMPP

  1. 好处: 源码开源, 可以自行拓展功能, 网上也有很多案例
  2. 缺点: 自己和后台人员需要做很多的操作(后台需要额外提供一些接口), 聊天服务器的稳定性可能不够好(看公司自己的运维人员技术是否够好), XML会耗流量

3. 自定义协议: 大型公司/专业即时通讯公司

  1. 好处: 接口可以自定义, 可以使用低流量的传输格式
  2. 缺点: 需要一定的自定义协议的经验, 包括对数据处理的经验, 对技术能力有一定的要求

二. 环信集成

1. 环信SDK介绍

环信V3版本使用了自定义协议

环信之前的版本是基于XMPP封装的

APP 服务器与环信服务器的集成

环信只是即时通讯的消息通道。环信本身不提供用户体系,环信既不保存任何 APP 业务数据,也不保存任何 APP 的用户信息。比如说,你的 APP 是一个婚恋交友 APP,那么你的 APP 用户的头像、昵称、身高、体重、三围、电话号码等信息是保存在你自己的 APP 业务服务器上,这些信息不需要告诉环信,环信也不想知道。

环信这样设计的目的有2个:

  1. 自己公司一定会有后台服务器, 可以存储用户的数据
  2. 用户数据非常核心, 不应该保存, 也不太敢存到其他地方

环信服务器提供了 REST API 服务用来集成用户和好友体系:

  1. 环信提供API, 快速将公司自己的账号体系, 转换成环信账号体系
  2. 环信也提供了好友体系(正常开发中, 不要使用.我们目前为了方便, 可以使用)
  3. 集成SDK
  4. 环信初始化&UI搭建
  1. - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
  2. {
  3. //AppKey:注册的AppKey,详细见下面注释。
  4. //apnsCertName:推送证书名(不需要加后缀),详细见下面注释。
  5. EMOptions *options = [EMOptions optionsWithAppkey:@"czbk#hmwechat"];
  6. options.apnsCertName = nil;
  7. [[EMClient sharedClient] initializeSDKWithOptions:options];
  8. return YES;
  9. }
  10. // APP进入后台
  11. - (void)applicationDidEnterBackground:(UIApplication *)application
  12. {
  13. //之后的消息, 应该发送远程推送
  14. [[EMClient sharedClient] applicationDidEnterBackground:application];
  15. }
  16. // APP将要从后台返回
  17. - (void)applicationWillEnterForeground:(UIApplication *)application
  18. {
  19. //之后的消息, 取消远程推送
  20. [[EMClient sharedClient] applicationWillEnterForeground:application];
  21. }

4. 注册&登录&退出

注册&登录

@ #warning 将来注册和登录, 应该调用服务器的接口, 这里只是为了方便测试, 使用的环信接口

  1. - (IBAction)loginClick:(id)sender {
  2. EMError *error = [[EMClient sharedClient] loginWithUsername:self.usernameTF.text password:self.passwordTF.text];
  3. if (!error) {
  4. NSLog(@"登录成功");
  5. //跳转根控制器
  6. UIStoryboard *mainSB = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
  7. HMTabBarController *tabBarC = [mainSB instantiateViewControllerWithIdentifier:@"HMTabBar"];
  8. [UIApplication sharedApplication].keyWindow.rootViewController = tabBarC;
  9. }
  10. }
  11. - (IBAction)registerClick:(id)sender {
  12. /*
  13. 注册模式分两种,开放注册和授权注册。
  14. 只有开放注册时,才可以客户端注册。开放注册是为了测试使用,正式环境中不推荐使用该方式注册环信账号。
  15. 授权注册的流程应该是您服务器通过环信提供的 REST API 注册,之后保存到您的服务器或返回给客户端。
  16. */
  17. EMError *error = [[EMClient sharedClient] registerWithUsername:self.usernameTF.text password:self.passwordTF.text];
  18. if (error==nil) {
  19. NSLog(@"注册成功");
  20. }
  21. }

退出&被动退出

  1. - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
  2. if (indexPath.section == 3) {
  3. /*
  4. 退出登录分两种类型:主动退出登录和被动退出登录。
  5. 主动退出登录:调用 SDK 的退出接口;
  6. 被动退出登录:1. 正在登录的账号在另一台设备上登录;2. 正在登录的账号被从服务器端删除。
  7. logout:YES: (远程推送)是否解除 device token 的绑定,在被动退出时 SDK 内部处理,需要调用退出方法。
  8. */
  9. EMError *error = [[EMClient sharedClient] logout:YES];
  10. if (!error) {
  11. NSLog(@"退出成功");
  12. //切换到登陆界面
  13. UIStoryboard *mainSB = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
  14. HMLoginViewController *loginVC = [mainSB instantiateViewControllerWithIdentifier:@"HMLogin"];
  15. [UIApplication sharedApplication].keyWindow.rootViewController = loginVC;
  16. }
  17. }
  18. }
  19. //添加回调监听代理:
  20. [[EMClient sharedClient] addDelegate:self delegateQueue:nil];
  21. #pragma mark - 被动退出
  22. /*!
  23. * 当前登录账号在其它设备登录时会接收到该回调
  24. */
  25. - (void)userAccountDidLoginFromOtherDevice {
  26. EMError *error = [[EMClient sharedClient] logout:NO];
  27. if (!error) {
  28. NSLog(@"退出成功");
  29. //切换到登陆界面
  30. UIStoryboard *mainSB = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
  31. HMLoginViewController *loginVC = [mainSB instantiateViewControllerWithIdentifier:@"HMLogin"];
  32. [UIApplication sharedApplication].keyWindow.rootViewController = loginVC;
  33. }
  34. }
  35. /*!
  36. * 当前登录账号已经被从服务器端删除时会收到该回调
  37. */
  38. - (void)userAccountDidRemoveFromServer {
  39. EMError *error = [[EMClient sharedClient] logout:NO];
  40. if (!error) {
  41. NSLog(@"退出成功");
  42. //切换到登陆界面
  43. UIStoryboard *mainSB = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
  44. HMLoginViewController *loginVC = [mainSB instantiateViewControllerWithIdentifier:@"HMLogin"];
  45. [UIApplication sharedApplication].keyWindow.rootViewController = loginVC;
  46. }
  47. }

5. 自动登录&自动重连

@\ #warning 将来注册和登录, 应该调用服务器的接口, 这里只是为了方便测试, 使用的环信接口

  1. - (IBAction)loginClick:(id)sender {
  2. EMError *error = [[EMClient sharedClient] loginWithUsername:self.usernameTF.text password:self.passwordTF.text];
  3. if (!error) {
  4. NSLog(@"登录成功");
  5. //设置自动登录
  6. [[EMClient sharedClient].options setIsAutoLogin:YES];
  7. }
  8. }
  9. /*
  10. 自动登录在以下几种情况下会被取消:
  11. 用户调用了 SDK 的登出动作;
  12. 用户在别的设备上更改了密码,导致此设备上自动登录失败;
  13. 用户的账号被从服务器端删除;
  14. 用户从另一个设备登录,把当前设备上登录的用户踢出。
  15. 所以,在您调用登录方法前,应该先判断是否设置了自动登录,如果设置了,则不需要您再调用。
  16. */
  17. //判断是否有自动登录
  18. BOOL isAutoLogin = [EMClient sharedClient].options.isAutoLogin;
  19. if (!isAutoLogin) {
  20. //跳转登录界面
  21. UIStoryboard *mainSB = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
  22. HMLoginViewController *loginVC = [mainSB instantiateViewControllerWithIdentifier:@"HMLogin"];
  23. //如果是AppDelegate里, 切换控制器应该使用self.window
  24. self.window.rootViewController = loginVC;
  25. }

@\ #pragma mark - 自动重连

  1. /*!
  2. * SDK连接服务器的状态变化时会接收到该回调
  3. *
  4. * 有以下几种情况,会引起该方法的调用:
  5. * 1. 登录成功后,手机无法上网时,会调用该回调
  6. * 2. 登录成功后,网络状态变化时,会调用该回调
  7. *
  8. * @param aConnectionState 当前状态
  9. */
  10. - (void)connectionStateDidChange:(EMConnectionState)aConnectionState {
  11. NSLog(@"重连状态: %zd", aConnectionState);
  12. //将来断网了可以弹出一些提示/或者UI发生一些改变, 让用户知道断网了
  13. }

三. 好友关系

注:环信不是好友也可以聊天,不推荐使用环信的好友机制。如果你有自己的服务器或好友关系,请自己维护好友关系。

将来开发中, 记得使用公司的接口实现

1. 添加好友

  1. #pragma mark - UITextFieldDelegate
  2. - (BOOL)textFieldShouldReturn:(UITextField *)textField {
  3. EMError *error = [[EMClient sharedClient].contactManager addContact:textField.text message:@"我想加您为好友"];
  4. if (!error) {
  5. NSLog(@"添加成功");
  6. [self.navigationController popViewControllerAnimated:YES];
  7. }
  8. return YES;
  9. }
  10. //注册好友回调
  11. [[EMClient sharedClient].contactManager addDelegate:self delegateQueue:nil];

2. 接受好友

  1. #pragma mark - 好友添加
  2. /*!
  3. * 用户A发送加用户B为好友的申请,用户B会收到这个回调
  4. *
  5. * @param aUsername 用户名
  6. * @param aMessage 附属信息
  7. */
  8. - (void)friendRequestDidReceiveFromUser:(NSString *)aUsername
  9. message:(NSString *)aMessage {
  10. //这里暂时弹出一个提示框, 让用户选择同意, 或者拒绝
  11. //同意好友请求
  12. EMError *error = [[EMClient sharedClient].contactManager acceptInvitationForUsername:aUsername];
  13. if (!error) {
  14. NSLog(@"同意请求");
  15. }
  16. //拒绝好友请求
  17. EMError *error = [[EMClient sharedClient].contactManager declineInvitationForUsername:aUsername];
  18. if (!error) {
  19. NSLog(@"拒绝请求");
  20. }
  21. }
  22. /*!
  23. @method
  24. @brief 用户A发送加用户B为好友的申请,用户B同意后,用户A会收到这个回调
  25. */
  26. - (void)friendRequestDidApproveByUser:(NSString *)aUsername {
  27. UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"好友通知" message:[NSString stringWithFormat:@"%@已经成为您的好友", aUsername] preferredStyle:UIAlertControllerStyleAlert];
  28. [self.window.rootViewController presentViewController:alertController animated:YES completion:nil];
  29. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  30. [alertController dismissViewControllerAnimated:YES completion:nil];
  31. });
  32. }
  33. /*!
  34. @method
  35. @brief 用户A发送加用户B为好友的申请,用户B拒绝后,用户A会收到这个回调
  36. */
  37. - (void)friendRequestDidDeclineByUser:(NSString *)aUsername {
  38. UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"好友通知" message:[NSString stringWithFormat:@"%@拒绝和您成为好友", aUsername] preferredStyle:UIAlertControllerStyleAlert];
  39. [self.window.rootViewController presentViewController:alertController animated:YES completion:nil];
  40. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  41. [alertController dismissViewControllerAnimated:YES completion:nil];
  42. });
  43. }

3. 好友列表

  1. //暂时模拟实时刷新
  2. - (void)viewDidAppear:(BOOL)animated {
  3. [super viewDidAppear:animated];
  4. //已进入就刷新好友数据
  5. [self reloadContactData];
  6. }
  7. #pragma mark - 从服务器获取所有的好友
  8. - (void)reloadContactData {
  9. EMError *error = nil;
  10. self.contactArray = [[EMClient sharedClient].contactManager getContactsFromServerWithError:&error];
  11. [self.tableView reloadData];
  12. }
  13. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  14. static NSString *cellID = @"contactCell";
  15. UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID forIndexPath:indexPath];
  16. cell.textLabel.text = self.contactArray[indexPath.row];
  17. return cell;
  18. }

添加好友

//发送通知, 让通讯录界面刷新

4. 删除好友

  1. - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
  2. if (editingStyle == UITableViewCellEditingStyleDelete) {
  3. // 删除好友
  4. //deleteContact: 要删除的用户
  5. //isDeleteConversation: 是否删除对应的会话和消息 删除缓存
  6. EMError *error = [[EMClient sharedClient].contactManager deleteContact:self.contactArray[indexPath.row] isDeleteConversation:YES];
  7. if (!error) {
  8. NSLog(@"删除成功");
  9. [self reloadContactData];
  10. }
  11. }
  12. }

四. 单聊

1. 发送消息

@\ #pragma mark 发送消息

  1. - (BOOL)textFieldShouldReturn:(UITextField *)textField {
  2. //1. 构造文字消息
  3. EMTextMessageBody *body = [[EMTextMessageBody alloc] initWithText:textField.text];
  4. //来自于谁--我
  5. NSString *from = [[EMClient sharedClient] currentUsername];
  6. //生成Message
  7. //ConversationID: 会话ID, 用于表示和某人的聊天记录, 不传, 默认就会使用to的内容
  8. EMMessage *message = [[EMMessage alloc] initWithConversationID:nil from:from to:self.userName body:body ext:nil];
  9. message.chatType = EMChatTypeChat;// 设置为单聊消息
  10. //message.chatType = EMChatTypeGroupChat;// 设置为群聊消息
  11. //message.chatType = EMChatTypeChatRoom;// 设置为聊天室消息
  1. //2. 发送消息
  2. [[EMClient sharedClient].chatManager sendMessage:message progress:nil completion:^(EMMessage *message, EMError *error) {
  3. if (error) {
  4. NSLog(@"error: %@", error);
  5. } else {
  6. NSLog(@"发送成功");
  7. }
  8. }];
  9. //3.清空文本框
  10. textField.text = @"";
  11. [textField resignFirstResponder];
  12. return YES;
  13. }

2. 读取消息列表

  1. - (void)viewDidLoad {
  2. [super viewDidLoad];
  3. self.tableView.estimatedRowHeight = 90;
  4. self.tableView.rowHeight = UITableViewAutomaticDimension;
  5. self.messageArrayM = [NSMutableArray array];
  6. [self reloadMessageData:YES];
  7. }
  8. - (void)reloadMessageData:(BOOL)isFirst {
  9. /*
  10. 会话:操作聊天消息 EMMessage 的容器,在 SDK 中对应的类型是 EMConversation。
  11. 新建/获取一个会话
  12. 根据 conversationId 创建一个 conversation。
  13. getConversation:创建与8001的会话
  14. type:会话类型
  15. createIfNotExist:不存在是否创建
  16. EMConversationTypeChat 单聊会话
  17. EMConversationTypeGroupChat 群聊会话
  18. EMConversationTypeChatRoom 聊天室会话
  19. */
  20. EMConversation *conversation = [[EMClient sharedClient].chatManager getConversation:self.userName type:EMConversationTypeChat createIfNotExist:YES];
  21. /*!
  22. * 从数据库获取指定数量的消息,取到的消息按时间排序,并且不包含参考的消息,如果参考消息的ID为空,则从最新消息取
  23. *
  24. * @param aMessageId 参考消息的ID
  25. * @param count 获取的条数
  26. * @param aDirection 消息搜索方向
  27. * @param aCompletionBlock 完成的回调
  28. */
  29. [conversation loadMessagesStartFromId:nil count:isFirst ? 20 : 1 searchDirection:EMMessageSearchDirectionUp completion:^(NSArray *aMessages, EMError *aError) {
  30. if (aError) {
  31. NSLog(@"数据库查询消息有问题: %@", aError);
  32. } else {
  33. [self.messageArrayM addObjectsFromArray:aMessages];
  34. NSLog(@"self.messageArray: %@", self.messageArrayM);
  35. [self.tableView reloadData];
  36. }
  37. }];
  38. }
  1. //2. 发送消息
  2. [[EMClient sharedClient].chatManager sendMessage:message progress:nil completion:^(EMMessage *message, EMError *error) {
  3. if (error) {
  4. NSLog(@"error: %@", error);
  5. } else {
  6. NSLog(@"发送成功");
  7. //重新读取并刷新
  8. [self reloadMessageData:NO];
  9. }
  10. }];
  11. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  12. static NSString *leftID = @"leftCell";
  13. static NSString *rightID = @"rightCell";
  14. EMMessage *message = self.messageArrayM[indexPath.row];
  15. UITableViewCell *cell;
  16. //消息的方向可以去分, 是谁发的
  17. if (message.direction) {
  18. //接收的消息
  19. cell = [tableView dequeueReusableCellWithIdentifier:leftID forIndexPath:indexPath];
  20. } else {
  21. //发送的消息
  22. cell = [tableView dequeueReusableCellWithIdentifier:rightID forIndexPath:indexPath];
  23. }
  24. EMMessageBody *msgBody = message.body;
  25. switch (msgBody.type) {
  26. case EMMessageBodyTypeText:
  27. {
  28. // 收到的文字消息
  29. EMTextMessageBody *textBody = (EMTextMessageBody *)msgBody;
  30. UILabel *label = [cell viewWithTag:1002];
  31. label.text = textBody.text;
  32. }
  33. break;
  34. default:
  35. break;
  36. }
  37. return cell;
  38. }

3. 接收消息

  1. //注册消息回调
  2. [[EMClient sharedClient].chatManager addDelegate:self delegateQueue:nil];
  3. #pragma mark - 接收普通消息
  4. /*!
  5. @method
  6. @brief 接收到一条及以上非cmd消息
  7. */
  8. - (void)messagesDidReceive:(NSArray *)aMessages {
  9. //需要发送通知, 告诉正在聊天的控制器进行数据刷新
  10. //通知时, 可以传递消息的数量 --> 7
  11. [[NSNotificationCenter defaultCenter] postNotificationName:@"HMMessagesDidReceiveNotification" object:nil userInfo:@{@"messageCount": @(aMessages.count)}];
  12. NSLog(@"aMessages: %@", aMessages);
  13. }
  14. - (void)viewDidLoad {
  15. [super viewDidLoad];
  16. self.tableView.estimatedRowHeight = 90;
  17. self.tableView.rowHeight = UITableViewAutomaticDimension;
  18. self.messageArrayM = [NSMutableArray array];
  19. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(messagesDidReceiveNotification:) name:@"HMMessagesDidReceiveNotification" object:nil];
  20. //首次刷新抓20条数据
  21. [self reloadMessageDataWithCount:20];
  22. }
  23. //这里还有一个通知的方法, 通知的方法中需要根据消息的个数, 重新读取数据reloadMessageDataWithCount:7
  24. - (void)messagesDidReceiveNotification:(NSNotification *)notification{
  25. int count = [notification.userInfo[@"messageCount"] intValue];
  26. [self reloadMessageDataWithCount:count];

IM——技术方案的更多相关文章

  1. 分布式锁1 Java常用技术方案

    前言:       由于在平时的工作中,线上服务器是分布式多台部署的,经常会面临解决分布式场景下数据一致性的问题,那么就要利用分布式锁来解决这些问题.所以自己结合实际工作中的一些经验和网上看到的一些资 ...

  2. unity小地图技术方案总结

    技术方案 一:从顶视图获取实时小地图(优点实现快容易对地图进行放大缩小操作而且地图,缺点是不好对地图做出修改,只能在顶部加个另外的相机层来遮盖) 1.创建Redertexture并改名为smallma ...

  3. iOS多线程技术方案

    iOS多线程技术方案 目录 一.多线程简介 1.多线程的由来 2.耗时操作的模拟试验 3.进程和线程 4.多线程的概念及原理 5.多线程的优缺点和一个Tip 6.主线程 7.技术方案 二.Pthrea ...

  4. Facebook存储技术方案:找出“暖性BLOB”数据

    Facebook公司已经在其近线存储体系当中彻底弃用RAID与复制机制,转而采用分布式擦除编码以隔离其所谓的“暖性BLOB”. 暖性?BLOB?这都是些什么东西?大家别急,马上为您讲解: BLOB—— ...

  5. 分布式锁1 Java常用技术方案(转)

    转:http://www.cnblogs.com/PurpleDream/p/5559352.html#3450419 前言:       由于在平时的工作中,线上服务器是分布式多台部署的,经常会面临 ...

  6. Android基于WIFI实现电脑和手机间数据传输的技术方案研究

    Android手机和电脑间基于wifi进行数据传输,从技术上讲,主要有两种方案: 一种是通过ftp协议实现,Android手机作为数据传输过程中的ftp服务器: 一种是通过http协议实现.Andro ...

  7. Binder Proxy技术方案

    Binder Proxy技术方案 作者 低端码农 时间 2014.08.23 0x0 看到有多朋友尝试通过hook系统进程system_process的ioctl,以企图截获系统的IPC通讯.这个方法 ...

  8. 技术方案:在外部网址调试本地js(基于fiddler)

    1 解决的问题 1)        场景1:生产环境报错 对前台开发来说,业务逻辑都在js中,所以报错90%以上都是js问题. 如果生产环境出现报错,但是测试环境正常.这时修改了代码没有环境验证效果, ...

  9. 分布式锁2 Java非常用技术方案探讨之ZooKeeper

    前言:       由于在平时的工作中,线上服务器是分布式多台部署的,经常会面临解决分布式场景下数据一致性的问题,那么就要利用分布式锁来解决这些问题.以自己结合实际工作中的一些经验和网上看到的一些资料 ...

  10. 全面解密QQ红包技术方案:架构、技术实现、移动端优化、创新玩法等

    本文来自腾讯QQ技术团队工程师许灵锋.周海发的技术分享. 一.引言 自 2015 年春节以来,QQ 春节红包经历了企业红包(2015 年).刷一刷红包(2016 年)和 AR 红包(2017 年)几个 ...

随机推荐

  1. Ionic3 UI组件之 ImageViewer

    组件特性: 轻触图片可全屏查看 手势上下滑动可关闭全屏查看 点击导航箭头可关闭视图 双击查看全图,并可放大 参考地址:https://github.com/Riron/ionic-img-viewer ...

  2. 关于Mysql+EF6本地运行和发布没有问题,发布到服务器上出现问题的解决方案

    这个问题折磨了我差不多两天,网上各种方法也找了个遍,但是都没有解决我的问题,后面通过自己仔细分析问题和排查,终于把问题解决了,以下是我的解决问题的步骤,希望能帮到各位,不要再被这些问题坑了 1,项目实 ...

  3. FOR XML PATH做为数据表中单列或者多列的字符串拼接的方法,放到一列中去,很好用。

    先看看自己弄得例子,SELECT sName+',',hoppy+','  FROM student2 where hoppy='游泳' FOR XML PATH('')--PATH后面跟的是行标题, ...

  4. Windows应用程序对键盘与鼠标的响应

      编写程序: 设计一个窗口, 当单击鼠标左键时, 窗口中显示"LEFT BUTTON"; 当单击鼠标右键时, 窗口中显示"RIGHT BUTTON"; 当单击 ...

  5. React—Native开发之原生模块向JavaScript发送事件

    首先,由RN中文网关于原生模块(Android)的介绍可以看到,RN前端与原生模块之 间通信,主要有三种方法: (1)使用回调函数Callback,它提供了一个函数来把返回值传回给JavaScript ...

  6. Flutter 案例学习之:GridView

    GitHub:https://github.com/happy-python/flutter_demos/tree/master/gridview_demo 在 ListView 中,如果将屏幕的方向 ...

  7. Android微信支付—注意事项

    坑点一:PayReq的参数 sign的生成 PayReq对象有个参数为packageValue 而sign生成时要用到packageValue,但是对应的Key是package,这里的key容易弄错 ...

  8. java 内存分析之堆栈空间

    package Demo; public class Demo { public static void main(String[] args) { Demo demo = new Demo(); ; ...

  9. LeetCode 题解之Add Two Numbers II

    1.题目描述 2.分析 首先将链表翻转,然后做加法. 最后将结果链表翻转. 3.代码 ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) { Lis ...

  10. 4.Spring——xml配置文件

    如果使用Maven构建项目,spring在加载xsd文件时总是先试图在本地查找xsd文件(spring的jar包中已经包含了所有版本的xsd文件), 如果没有找到,才会转向去URL指定的路径下载.ap ...