注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

项目想美观么?试试它吧!!自定义加载视图:mkloader

美丽流畅的自定义加载视图 使用<com.tuyenmonkey.mkloader.MKLoader android:layout_width="wrap_content" android:layout_heigh...
继续阅读 »

美丽流畅的自定义加载视图



使用

<com.tuyenmonkey.mkloader.MKLoader
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:mk_type="<loading_type>" (Optional. Default is ClassicSpinner)
app:mk_color="<color>" (Optional. Default is #ffffff)
/>

类型


  • Sharingan
  • TwinFishesSpinner
  • ClassicSpinner
  • LineSpinner
  • FishSpinner
  • PhoneWave
  • ThreePulse
  • FourPulse
  • FivePulse
  • Worm
  • Whirlpool
  • Radar

安装

Gradle


dependencies {
compile 'com.tuyenmonkey:mkloader:1.4.0'
}

github地址:https://github.com/nntuyen/mkloader


下载地址:master.zip


收起阅读 »

iOS应用程序瘦身的静态库解决方案

为什么要给程序瘦身?随着应用程序的功能越来越多,实现越来越复杂,第三方库的引入,UI体验的优化等众多因素程序中的代码量成倍的增长,从而导致应用程序包的体积越来越大。当程序体积变大后不仅会出现编译流程变慢,而且还会出现运行性能问题,会增加应用下载时长和消耗用户的...
继续阅读 »

为什么要给程序瘦身?

随着应用程序的功能越来越多,实现越来越复杂,第三方库的引入,UI体验的优化等众多因素程序中的代码量成倍的增长,从而导致应用程序包的体积越来越大。当程序体积变大后不仅会出现编译流程变慢,而且还会出现运行性能问题,会增加应用下载时长和消耗用户的移动网络流量等等。因此在这些众多的问题下需要对应用进行瘦身处理。

一个应用程序由众多资源文件和可执行程序文件组成,资源文件的优化不在本文探讨范围。本文主要讨论对可执行程序代码瘦身的方法。

对可执行程序代码瘦身主要就是想办法让程序中不会被调用的源代码不参与编译或链接。我们可以通过一些源代码分析工具来查找哪些函数或者类方法没有被调用并从代码中删除掉来解决编译链接前的瘦身问题。这些分析工具也不在本文的讨论范围内。应用程序在编译时会对工程中的所有代码都执行编译处理并生成目标文件。而在链接阶段则会根据程序代码中对符号的引用关系来将所有相关的目标文件链接为一个大的可执行程序文件,并且在链接阶段链接器会优化掉所有没被调用的C/C++函数代码,但是对于OC类中的没有调用的方法则不会被优化掉。所以为了对可执行程序在编译链接阶段进行瘦身处理就需要了解源代码的编译链接规则。这也是本文所要介绍的针对工程通过静态库的形式进行编译和链接的方式来减少可执行程序代码的尺寸。您可以从文章:《深入iOS系统底层之静态库介绍》中详细的了解到静态库的编译链接过程,以及相关的技术细节。

一个瘦身的例子!

为了验证和具体的实践,我在github上建立了一个项目:YSAppSizeTest。您可以从这个项目中看到如何对工程进行构建以实现程序的瘦身处理。

在示例项目中同一个Workspace中分别建立ThinApp和FatApp两个工程,这两个工程实现的功能是一样。在整个应用程序中分别定义了CA、CB、CC、CD、CE一共5个OC类,定义了一个UIView(Test)分类,还有定义了两个C函数:libFoo1和libFoo1。

整个应用程序中只使用了CA和CC两个OC类,以及调用了UIView(Test)分类方法,以及调用了libFoo1函数,并且同时都采用导入静态库的形式。因为这两个工程对文件的定义和分布策略不同使得两个应用程序的最终可执行代码的尺寸是不相同的。

FatApp中的文件定义和分布策略

1、FatApp工程依赖并导入了FatAppLib静态库工程。
2、CA,CB两个类都定义在主程序工程中。
3、CC,CD,CE三个类,以及UIView(Test)分类,还有libFoo1,libFoo2两个函数都定义在FatAppLib静态库工程中。
4、CC,CD两个类定义在同一个文件中,CE类则定义在单独的文件中。
5、FatApp工程的Other Linker Flags中设置了 -ObjC选项。

ThinApp中的文件定义和分布策略

1、ThinApp工程依赖并导入了ThinAppLib静态库工程。
2、主程序工程就是一个壳工程。
3、CA,CB,CC,CD,CE5个类,以及UIView(Test)分类,还有libFoo1,libFoo2两个函数都定义在ThinAppLib静态库工程中。
4、上述的5个类都分别定义在不同的文件中。
5、ThinApp工程的Other Linker Flags中没有设置-ObjC选项。

上述两个工程的程序被Archive出来后,FatApp可执行程序的尺寸是367KB,而ThinApp可执行程序的尺寸是334KB。通过一些工具比如Mach-O View或者 IDA可以看出:FatApp中5个OC类的代码以及libFoo1函数还有UIView(Test)分类的代码都被链接进可执行程序中;而ThinApp中则只有CA,CC两个类以及libFoo1函数还有UIView(Test)分类的代码被链接进可执行程序中。在ThinApp中虽然没有使用-Objc链接选项,但是静态库中的分类也被链接进可执行程序中。

应用程序工程构建规则

根据对项目中的文件定义和引用策略以及相关的理论基础我们可以按照如下的规则来构建您的应用程序:

1、尽量将所有代码都移植到静态库中,而主程序则保留为一个壳程序。具体操作方法是建立一个Workspace,然后主程序工程就只有默认创建工程时的代码,所有新加入的代码都建立并存放到静态库工程中去,然后通过工程依赖来引入这些静态库工程,或者借助一些工程化工具比如Cocoapods来实现这种拆分和引用处理。主程序工程中只保留AppDelegate的代码,其他代码都一致到静态库中。然后在AppDelegate中的相关代码处调用静态库中定义的业务代码。

2、按业务组件对工程进行解耦每个组件是一个静态库工程。静态库中的每一个文件中最好只有一个类的实现,并且类的分类实现最好和类实现编写在同一个文件中,相同功能的代码以及可能都会被调用的代码尽量存放在一个文件中。

3、不要在主程序工程中使用-ObjC和-all_load两个选项而改为用-force_load 来单独指定要执行加载的静态库。-ObjC和-all_load选项会把主程序工程以及所依赖的所有静态库中的工程中的全部代码都链接到可执行程序中而不管代码是否有被调用过或者使用过。而force_load则只会将指定的静态库中的所有代码链接到可执行程序中,当然force_load如果没有必要也尽量不要使用。

4、尽量减少在静态库中定义OC类的分类方法,如果一定要定义分类方法则可以将分类方法定义在和类定义相同的文件中,或者将分类方法定义在一个一定会被调用和引用的实现文件中。因为根据链接规则静态库中的分类是不会被链接进可执行程序中的,除非使用了上述的三个链接选项。如果将分类代码单独的定义在一个文件中的话则可以通过在分类的头文件中定义一个内联函数,内联函数调用分类实现文件中的一个dumy函数,这样只要这个分类的头文件被include或者import就会把整个分类的实现链接到可执行程序中去。一般情况下我们在静态库中建立分类那就表明一定会被某个文件引用这个分类,从而实现整个文件的链接处理。在分类中定义的这两个函数则因为没有被任何地方调用,因此会在链接优化中将这两个函数给优化掉。这样就使得即使我们不用-ObjC选项也能将静态库中的分类链接到可执行程序中去。最后需要注意的是在每个分类中定义的这两个函数名最好能够唯一这样就不会出现符号重名冲突的问题了。

//分类文件的头文件UIView+XXX.h
@interface UIView (XXX)

//分类中定义的方法

@end

/*
通过在分类的头文件中定义一个内联函数,内联函数调用分类实现文件中的一个dumy函数,这样只要这个分类的头文件被include或者import就会把
整个分类的实现链接到可执行程序中去。一般情况下我们在静态库中建立分类那就表明一定会被某个文件引用这个分类,从而实现整个文件的链接处理。
而在分类中定义的这两个函数则因为没有被任何地方调用,因此会在链接优化中将这两个函数给优化掉。这样就使得即使我们不用-ObjC选项也能
将静态库中的分类链接到可执行程序中去。最后需要注意的是在每个分类中定义的这两个函数名最好能够唯一这样就不会出现符号重名冲突的问题了。
*/
extern void _cat_UIView_XXX_Impl(void);
inline void _cat_UIView_XXX_Decl(void){_cat_UIView_XXX_Impl();}


------------------------------------------------------------
//分类文件的实现文件UIView+XXX.m
#import "UIView+XXX.h"

@implementation UIView (XXX)

//分类的实现代码

@end

void _cat_UIView_XXX_Impl(void){}


---------------------------------------------------------------
//最后把这个分类头文件放入到某个对外暴露的头文件中,比如本例中将分类代码放入到了ThinAppLib.h文件中
//ThinAppLib.h

#import "UIView+XXX.h"
//其他头文件

5、除了可以通过-force_load来加载指定静态库中的所有代码外。我们还可以在构建静态库时,在静态库的工程的Build Settings中将Perform Single-Object Prelink 中的开关选项打开。当这个开关打开时,系统会对生成的静态库的所有目标文件执行预链接操作,预链接操作会将所有的目标文件组合成为一个单独的大的目标文件。这样根据以文件为单位的链接规则就会将静态库中的所有代码全部都链接进可执行程序中去,但是这样带来的问题就是最后在dead code stripping时删除不掉已经链接进来的那些没有被任何地方使用过的OC类了。

6、对于引入的一些第三方静态库或者第三方的开源库来说因为我们无法去改变其实现逻辑。如果这个静态库中没有任何分类代码的定义则正常引用即可,如果静态库中有分类方法的定义则单独对这个静态库采用-force_load选项。

总之一句话:为了让你的程序瘦身,尽量将代码放到静态库中,不要使用-Objc和-all_load选项

为了验证上述方法的有效性,笔者对项目中的应用做了一个测试:分别是有带-ObjC选项和没有带-ObjC选项的情况下的应用程序包中可执行程序的大小从115M减少到95M,减少了20M的尺寸。

链接:https://www.jianshu.com/p/2078e00891fd

收起阅读 »

日!!聊天页面还能这么简单??ChatKit

ChatKit 是一个免费且开源的 UI 聊天组件,由 LeanCloud 官方推出,底层聊天服务基于 LeanCloud 的 IM 即时通讯服务 LeanMessage 而开发。它的最大特点是把聊天常用的一些功能配合 UI 一起提供给开发者,帮助开发者快速集...
继续阅读 »

ChatKit 是一个免费且开源的 UI 聊天组件,由 LeanCloud 官方推出,底层聊天服务基于 LeanCloud 的 IM 即时通讯服务 LeanMessage 而开发。它的最大特点是把聊天常用的一些功能配合 UI 一起提供给开发者,帮助开发者快速集成 IM 服务,轻松实现聊天功能。
+


ChatKit 开源且提供完全自由的授权协议,开发者可以对其进行任意修改、扩展和二次封装,但是 LeanCloud 并不对 ChatKit 的二次开发提供技术支持。

+


获取项目


git clone git@github.com:leancloud/LeanCloudChatKit-Android.git

运行 Demo


获取源代码之后,用 Android Studio 打开项目,左侧的 Project 视图显示为:

+




「ChatKit-Android」Project 包含两个模块:

+



  • leancloudchatkit

    是一个封装了 LeanCloud 即时通讯的 UI lib,其目的是让开发者更快速地接入 LeanCloud 即时通讯的功能。

  • chatkitapplication

    为 Demo 项目,它是一个简单的示例项目,用来指导开发者如何使用 leancloudchatkit。


然后,请确保电脑上已经连接了一台真机或者虚拟机用作调试。

+


点击 Debug 或者 Run,第一次启动会运行 Gradle Build。建议打开全局网络代理,否则 Gradle Build 可能会因为网络原因无法完成。

+


使用方法


开发者可以将 ChatKit 导入到自己的 Project 中使用。下面我们将新建一个 Project(名为 ChatDemo) 用以导入 ChatKit。导入方式推荐通过源代码导入

+


源代码导入



  1. 浏览器访问 https://github.com/leancloud/LeanCloudChatKit-Android

  2. 执行以下命令行,将项目 clone 到本地(如 ChatKit 文件夹中,或者直接下载 zip 包自行解压缩到此文件夹下):
    git clone https://github.com/leancloud/LeanCloudChatKit-Android.git`


  3. 将文件夹 leancloudchatkit 复制到 ChatDemo 根目录;

  4. 修改 ChatDemo/settings.gradle 加入 include ':leancloudchatkit'

  5. 修改 ChatDemo/app/build.gradle,在 dependencies 中添加 compile project(":leancloudchatkit")


最后只要 Sync Project,这样 ChatKit 就算是导入到项目中了。

+


自定义使用


一、实现自己的 Application

+


ChatKit 在使用之前需要进行初始化,就像直接使用 LeanCloud 基础 SDK 时需要调用 AVOSCloud.initialize(appId, appKey) 一样。初始化逻辑应该放在 Application.onCreate 方法中实现。

+


ChatDemo 中新建一个 Class,名字叫做 ChatDemoApplication,让它继承自 Application 类,代码如下:

+


public class ChatDemoApplication extends Application {

// appId、appKey 可以在「LeanCloud 控制台 > 设置 > 应用 Key」获取
private final String APP_ID = "********";
private final String APP_KEY = "********";

@Override
public void onCreate() {
super.onCreate();
// 关于 CustomUserProvider 可以参看后面的文档
LCChatKit.getInstance().setProfileProvider(CustomUserProvider.getInstance());
LCChatKit.getInstance().init(getApplicationContext(), APP_ID, APP_KEY);
}
}

二、在 AndroidMainfest.xml 中配置 ChatDemoApplication

+


<application
...
android:name=".ChatDemoApplication" >
...
</application>

三、实现自己的用户体系

+


一般来说,聊天界面要相对复杂一些,不但要支持文字、表情、图片、语音等消息格式,还要展示用户信息,比如用户的头像、昵称等。而 LeanCloud 的消息流中只包含用户的 clientId 这一唯一标识,所以要获取头像这类额外的用户信息,就需要开发者接入自己的用户系统来实现。

+


为了保证通用性和扩展性,让开发者可以更容易将聊天界面嵌入自己的应用中,ChatKit 在设计上抽象出了一个「用户体系」的接口,需要开发者自己提供用户信息的获取方式。该接口只有一个方法需要开发者实现:

+


/**
* 用户体系的接口,开发者需要实现此接口来接入 LCChatKit
*/

public interface LCChatProfileProvider {
// 根据传入的 clientId list,查找、返回用户的 Profile 信息(id、昵称、头像)
public void fetchProfiles(List<String> userIdList, LCChatProfilesCallBack profilesCallBack);
}

为此,我们在 ChatDemo 中新建一个 Class,名字叫做 CustomUserProvider,代码如下:

+


public class CustomUserProvider implements LCChatProfileProvider {

private static CustomUserProvider customUserProvider;

public synchronized static CustomUserProvider getInstance() {
if (null == customUserProvider) {
customUserProvider
= new CustomUserProvider();
}
return customUserProvider;
}

private CustomUserProvider() {
}

private static List<LCChatKitUser> partUsers = new ArrayList<LCChatKitUser>();

// 此数据均为模拟数据,仅供参考
static {
partUsers
.add(new LCChatKitUser("Tom", "Tom", "http://www.avatarsdb.com/avatars/tom_and_jerry2.jpg"));
partUsers
.add(new LCChatKitUser("Jerry", "Jerry", "http://www.avatarsdb.com/avatars/jerry.jpg"));
partUsers
.add(new LCChatKitUser("Harry", "Harry", "http://www.avatarsdb.com/avatars/young_harry.jpg"));
partUsers
.add(new LCChatKitUser("William", "William", "http://www.avatarsdb.com/avatars/william_shakespeare.jpg"));
partUsers
.add(new LCChatKitUser("Bob", "Bob", "http://www.avatarsdb.com/avatars/bath_bob.jpg"));
}

@Override
public void fetchProfiles(List<String> list, LCChatProfilesCallBack callBack) {
List<LCChatKitUser> userList = new ArrayList<LCChatKitUser>();
for (String userId : list) {
for (LCChatKitUser user : partUsers) {
if (user.getUserId().equals(userId)) {
userList
.add(user);
break;
}
}
}
callBack
.done(userList, null);
}

public List<LCChatKitUser> getAllUsers() {
return partUsers;
}
}

当用户昵称和头像需要更新时,需要覆盖旧的 LCChatKitUser 对象并更新本地缓存:

+


    LCChatKitUser user = new LCChatKitUser("唯一 userId 不可变", "要变更的昵称", "要变更的 avatarURL");
LCIMProfileCache.getInstance().cacheUser(user);

四、打开即时通讯,并且跳转到聊天页面

+


我们支持通过两种方式来打开聊天界面:

+



  1. 通过指定另一个参与者的 clientId 的方式,开启一对一的聊天;

    此时,通过调用 intent.putExtra(LCIMConstants.PEER_ID, "peermemberId") 来传递另一参与者的 clientId。

  2. 通过指定一个已经存在的 AVIMConversation id 的方式,开启单人、多人或者开放式聊天室;

    此时,通过调用 LCIMConstants.CONVERSATION_ID, "particularConversationId") 来传递特定对话 Id。


下面的代码展示了如何通过第一种方式来开启聊天界面:

+


LCChatKit.getInstance().open("Tom", new AVIMClientCallback() {
@Override
public void done(AVIMClient avimClient, AVIMException e) {
if (null == e) {
finish
();
Intent intent = new Intent(MainActivity.this, LCIMConversationActivity.class);
intent
.putExtra(LCIMConstants.PEER_ID, "Jerry");
startActivity
(intent);
} else {
Toast.makeText(MainActivity.this, e.toString(), Toast.LENGTH_SHORT).show();
}
}
});

这样,Tom 就可以和 Jerry 愉快地聊天了。

+


接口以及组件


以下介绍在 ChatKit 中开发者常需要关注的业务逻辑组件(Interface)和界面组件(UI)。

+


用户


LCChatKitUser 是 ChatKit 封装的参与聊天的用户,它提供了如下属性:

2+























名称 描述
userId 用户在单个应用内唯一的 ID,也是调用 LCChatKit.open 时传入的 userId。
avatarUrl 用户头像的 URL
name 用户名

使用这些默认的属性基本可以满足一个聊天应用的需求,同时开发者可以通过继承 LCChatKitUser 类实现更多属性。具体用法请参考 Demo 中的 MembersAdapter.java

+


用户信息管理类


LCChatProfileProvider 接口需要用户 implements 后实现 fetchProfiles 函数,以使 ChatKit 在需要显示的时候展示用户相关的信息。

+


例如 Demo 中的 CustomUserProvider 这个类,它实现了 LCChatProfileProvider 接口,在 fetchProfiles 方法里加载了 Tom、Jerry 等人的信息。

+


核心类


LCChatKit 是 ChatKit 的核心类,具体逻辑可以参看代码,注意以下几个主要函数:

+



public void init(Context context, String appId, String appKey)

此函数用于初始化 ChatKit 的相关设置,此函数要在 Application 的 onCreate 中调用,否则可能会引起异常。

public void setProfileProvider(LCChatProfileProvider profileProvider)

此函数用于设置用户体系,因为 LeanCloud 即时通讯功能已经实现了完全剥离用户体系的功能,这里接入已有的用户体系会很方便。

public void open(final String userId, final AVIMClientCallback callback)

此函数用于开始即时通讯,open 成功后可以执行自己的逻辑,或者跳转到聊天页面。



对话列表界面


对话 AVIMConversation 是 LeanMessage 封装的用来管理对话中的成员以及发送消息的载体,不论是群聊还是单聊都是在一个对话当中;而对话列表可以作为聊天应用默认的首页显示出来,主流的社交聊天软件,例如微信,就是把最近的对话列表作为登录后的首页。

+


因此,我们也提供了对话列表 LCIMConversationListFragment 页面供开发者使用,在 Demo 项目中的 MainActivity 中的 initTabLayout 方法中演示了如何引入对话列表页面:

+


  private void initTabLayout() {
String[] tabList = new String[]{"会话", "联系人"};
final Fragment[] fragmentList = new Fragment[] {new LCIMConversationListFragment(),
new ContactFragment()};
// 以上这段代码为新建了一个 Fragment 数组,并且把 LCIMConversationListFragment 作为默认显示的第一个 Tab 页面

tabLayout
.setTabMode(TabLayout.MODE_FIXED);
for (int i = 0; i < tabList.length; i++) {
tabLayout
.addTab(tabLayout.newTab().setText(tabList[i]));
}
...
}

具体的显示效果如下:

+




聊天界面


聊天界面是显示频率最高的前端页面,ChatKit 通过 LCIMConversationFragmentLCIMConversationActivity 来实现这一界面。在 Demo 的 ContactItemHolder 界面包含了使用 LCIMConversationActivity 的实例:

+


  public void initView() {
nameView
= (TextView)itemView.findViewById(R.id.tv_friend_name);
avatarView
= (ImageView)itemView.findViewById(R.id.img_friend_avatar);

itemView
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 点击联系人,直接跳转进入聊天界面
Intent intent = new Intent(getContext(), LCIMConversationActivity.class);
// 传入对方的 Id 即可
intent
.putExtra(LCIMConstants.PEER_ID, lcChatKitUser.getUserId());
getContext
().startActivity(intent);
}
});
}

具体的显示效果如下:

+




联系人列表页面


因为 ChatKit 是与应用的用户体系完全解耦的,所以 ChatKit 中并没有包含联系人列表页面,但部分开发者可能有此需求,所以 chatkitapplication 实现了一个基于 LCChatProfileProvider 的联系人列表页面,具体代码可以参考 ContactFragment.java
具体效果如下:

+




常见问题


ChatKit 组件收费么?

ChatKit 是完全开源并且免费给开发者使用,使用聊天所产生的费用以账单为准。

+


接入 ChatKit 有什么好处?

它可以减轻应用或者新功能研发初期的调研成本,直接引入使用即可。ChatKit 从底层到 UI 提供了一整套的聊天解决方案。




收起阅读 »

罕见!!上弹窗:Alerter

alerter克服了toast和snackbar的缺点,并布局很简单 生成为了简单起见,Alerter采用了builder模式,以便于轻松集成到任何应用程序中。 可自定义的警报视图将动态添加到窗口的装饰视图中,覆盖所有内容。 安装配置allprojects {...
继续阅读 »

alerter克服了toast和snackbar的缺点,并布局很简单


生成

为了简单起见,Alerter采用了builder模式,以便于轻松集成到任何应用程序中。


可自定义的警报视图将动态添加到窗口的装饰视图中,覆盖所有内容。


安装配置

allprojects {
repositories {
jcenter()
maven { url "https://jitpack.io" }
}
}

build.gradle


dependencies {
implementation 'com.github.tapadoo:alerter:$current-version'
}

使用


Activity -


Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.show()

Fragment -


Alerter.create(activity)
.setTitle("Alert Title")
.setText("Alert text...")
.show()

展示 -


Alerter.isShowing()

隐藏 -


Alerter.hide()

定制

背景颜色

Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.setBackgroundColorRes(R.color.colorAccent) // or setBackgroundColorInt(Color.CYAN)
.show()


图标

Alerter.create(this@DemoActivity)
.setText("Alert text...")
.setIcon(R.drawable.alerter_ic_mail_outline)
.setIconColorFilter(0) // Optional - Removes white tint
.setIconSize(R.dimen.custom_icon_size) // Optional - default is 38dp
.show()


自定义屏幕持续时间(毫秒)

Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.setDuration(10000)
.show()

无标题

Alerter.create(this@DemoActivity)
.setText("Alert text...")
.show()


添加 Click Listener

 Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.setDuration(10000)
.setOnClickListener(View.OnClickListener {
Toast.makeText(this@DemoActivity, "OnClick Called", Toast.LENGTH_LONG).show();
})
.show()


长的文本

 Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setText("The alert scales to accommodate larger bodies of text. " +
"The alert scales to accommodate larger bodies of text. " +
"The alert scales to accommodate larger bodies of text.")
.show()


自定义进入/退出动画

  Alerter.create(this@KotlinDemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.setEnterAnimation(R.anim.alerter_slide_in_from_left)
.setExitAnimation(R.anim.alerter_slide_out_to_right)
.show()

可见性回调

 Alerter.create(this@KotlinDemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.setDuration(10000)
.setOnShowListener(OnShowAlertListener {
Toast.makeText(this@KotlinDemoActivity, "Show Alert", Toast.LENGTH_LONG).show()
})
.setOnHideListener(OnHideAlertListener {
Toast.makeText(this@KotlinDemoActivity, "Hide Alert", Toast.LENGTH_LONG).show()
})
.show()

自定义字体和文本外观

 Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setTitleAppearance(R.style.AlertTextAppearance_Title)
.setTitleTypeface(Typeface.createFromAsset(getAssets(), "Pacifico-Regular.ttf"))
.setText("Alert text...")
.setTextAppearance(R.style.AlertTextAppearance_Text)
.setTextTypeface(Typeface.createFromAsset(getAssets(), "ScopeOne-Regular.ttf"))
.show()


退出

 Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.enableSwipeToDismiss()
.show()


Progress Bar

 Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.enableProgress(true)
.setProgressColorRes(R.color.colorAccent)
.show()


带按钮

 Alerter.create(this@KotlinDemoActivity)
.setTitle(R.string.title_activity_example)
.setText("Alert text...")
.addButton("Okay", R.style.AlertButton, View.OnClickListener {
Toast.makeText(this@KotlinDemoActivity, "Okay Clicked", Toast.LENGTH_LONG).show()
})
.addButton("No", R.style.AlertButton, View.OnClickListener {
Toast.makeText(this@KotlinDemoActivity, "No Clicked", Toast.LENGTH_LONG).show()
})
.show()


自定义layout

 Alerter.create(this@KotlinDemoActivity, R.layout.custom_layout)
.setBackgroundColorRes(R.color.colorAccent)
.also { alerter ->
val tvCustomView = alerter.getLayoutContainer()?.tvCustomLayout
tvCustomView?.setText(R.string.with_custom_layout)
}
.show()


github地址:https://github.com/Tapadoo/Alerter
下载地址:master.zip


收起阅读 »

iOS开发你不知道的事-编译&链接

对于平常的应用程序开发,我们很少需要关注编译和链接过程。我们平常Xcode开发就是集成的的开发环境(IDE),这样的IDE一般都将编译和链接的过程一步完成,通常将这种编译和链接合并在一起的过程称为构建,即使使用命令行来编译一个源代码文件,简单的一句gcc he...
继续阅读 »

对于平常的应用程序开发,我们很少需要关注编译和链接过程。我们平常Xcode开发就是集成的的开发环境(IDE),这样的IDE一般都将编译和链接的过程一步完成,通常将这种编译和链接合并在一起的过程称为构建,即使使用命令行来编译一个源代码文件,简单的一句gcc hello.c命令就包含了非常复杂的过程!

正是因为集成开发环境的强大,很多系统软件的运行机制与机理被掩盖,其程序的很多莫名其妙的错误让我们无所适从,面对程序运行时种种性能瓶颈我们束手无策。我们看到的是这些问题的现象,但是却很难看清本质,所有这些问题的本质就是软件运行背后的机理及支撑软件运行的各种平台和工具,如果能深入了解这些机制,那么解决这些问题就能够游刃有余,收放自如了。

编译流程分析
现在我们通过一个C语言的经典例子,来具体了解一下这些机制:

#include <stdio.h>
int main(){
printf("Hello World");
return 0;
}

在linux下只需要一个简单的命令(假设源代码文件名为hello.c):

$ gcc hello.c
$ ./a.out
Hello World

其实上述过程可以分解为四步:

1、预处理(Prepressing)
2、编译(Compilation)
3、汇编(Assembly)
4、链接(Linking)


预编译
首先是源代码文件hello.c和相关的头文件(如stdio.h等)被预编译器cpp预编译成一个.i文件。第一步预编译的过程相当于如下命令(-E 表示只进行预编译):

$ gcc –E hello.c –o hello.i

还可以下面的表达

$ cpp hello.c > hello.i

预编译过程主要处理源代码文件中以”#”开头的预编译指令。比如#include、#define等,主要处理规则如下:

1、将所有的#define删除,并展开所有的宏定义
2、处理所有条件预编译指令,比如#if,#ifdef,#elif,#else,#endif
3、处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。
4、删除所有的注释//和/**/
5、添加行号和文件名标识,比如#2 “hello.c” 2。
6、保留所有的#pragma编译器指令
截图个大家看看效果


经过预编译后的文件(.i文件)不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经插入到.i文件中,所以当我们无法判断宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题。

编译(compliation)
编译过程就是把预处理完的文件进行一系列的:词法分析、语法分析、语义分析及优化后生产相应的汇编代码文件,此过程是整个程序构建的核心部分,也是最复杂的部分之一。其编译过程相当于如下命令:

$ gcc –S hello.i –o hello.s


通过上图我们不难得出,通过命令得到汇编输出文件hello.s.

汇编(assembly)
汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎对应一条机器令。所以汇编器的汇编过程相对于编译器来讲比较简单,它没复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译就可以了。其汇编过程相当于如下命令:

as hello.s –o hello.o

或者

gcc –c hello.s –o hello.o

或者使用gcc命令从C源代码文件开始,经过预编译、编译和汇编直接输出目标文件:

gcc –c hello.c –o hello.o

链接(linking)
链接通常是一个让人比较费解的过程,为什么汇编器不直接输出可执行文件而是输出一个目标文件呢?为什么要链接?下面让我们来看看怎么样调用ld才可以产生一个能够正常运行的Hello World程序:

注意默认情况没有gcc / 记得 :
$ brew install gcc

链接相应的库


下面在贴出我们的写出的源代码是如何变成目标代码的流程图:


主要通过我们的编译器做了以下任务:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化

到这我们就可以得到以下的文件,不知道你是否有和我一起操作,玩得感觉还是不错,继续往下面看


iOS的编译器
iOS现在为了达到更牛逼的速度和优化效果,采用了LLVM

1.LLVM核心库:
LLVM提供一个独立的链接代码优化器为许多流行CPU(以及一些不太常见的CPU)的代码生成支持。这些库是围绕一个指定良好的代码表示构建的,称为LLVM中间表示(“LLVM IR”)。LLVM还可以充当JIT编译器 - 它支持x86 / x86_64和PPC / PPC64程序集生成,并具有针对编译速度的快速代码优化。。

2.LLVM IR 生成器Clang: Clang是一个“LLVM原生”C / C ++ / Objective-C编译器,旨在提供惊人的快速编译(例如,在调试配置中编译Objective-C代码时比GCC快3倍),非常有用的错误和警告消息以及提供构建优秀源代码工具的平台。

3.LLDB项目:
LLDB项目以LLVM和Clang提供的库为基础,提供了一个出色的本机调试器。它使用Clang AST和表达式解析器,LLVM JIT,LLVM反汇编程序等,以便提供“正常工作”的体验。在加载符号时,它也比GDB快速且内存效率更高。

4.libc和libc++:
libc 和libc++ ABI项目提供了C ++标准库的标准符合性和高性能实现,包括对C ++ 11的完全支持。

5.lld项目:
lld项目旨在成为clang / llvm的内置链接器。目前,clang必须调用系统链接器来生成可执行文件。

LLVM采用三相设计,前端Clang负责解析,验证和诊断输入代码中的错误,然后将解析的代码转换为LLVM IR,后端LLVM编译把IR通过一系列改进代码的分析和优化过程提供,然后被发送到代码生成器以生成本机机器代码。


编译器前端的任务是进行:

1、语法分析
2、语义分析
3、生成中间代码(intermediate representation )

在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行。


以上图解内容所做的是事情和gcc编译一模模一样样!

iOS程序-详细编译过程
1.写入辅助文件:将项目的文件结构对应表、将要执行的脚本、项目依赖库的文件结构对应表写成文件,方便后面使用;并且创建一个 .app 包,后面编译后的文件都会被放入包中;
2.运行预设脚本:Cocoapods 会预设一些脚本,当然你也可以自己预设一些脚本来运行。这些脚本都在 Build Phases中可以看到;
3.编译文件:针对每一个文件进行编译,生成可执行文件 Mach-O,这过程 LLVM 的完整流程,前端、优化器、后端;
4.链接文件:将项目中的多个可执行文件合并成一个文件;
5.拷贝资源文件:将项目中的资源文件拷贝到目标包;
6.编译 storyboard 文件:storyboard 文件也是会被编译的;
7.链接 storyboard 文件:将编译后的 storyboard 文件链接成一个文件;
8.编译 Asset 文件:我们的图片如果使用 Assets.xcassets 来管理图片,那么这些图片将会被编译成机器码,除了 icon 和 launchImage;
9.运行 Cocoapods 脚本:将在编译项目之前已经编译好的依赖库和相关资源拷贝到包中。
10.生成 .app 包
11.将 Swift 标准库拷贝到包中
12.对包进行签名
13.完成打包

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

收起阅读 »

测试 View Controllers

我们不是迷信测试,但它应该帮助我们加快开发进度,并且让事情变得更有趣。让事情保持简单测试简单的事情很简单,同样,测试复杂的事会很复杂。就像我们在其他文章中指出的那样,让事情保持简单小巧总是好的。除此之外,它还有利于我们测试。这是件双赢的事。让我们来看看测试驱动...
继续阅读 »

我们不是迷信测试,但它应该帮助我们加快开发进度,并且让事情变得更有趣。

让事情保持简单

测试简单的事情很简单,同样,测试复杂的事会很复杂。就像我们在其他文章中指出的那样,让事情保持简单小巧总是好的。除此之外,它还有利于我们测试。这是件双赢的事。让我们来看看测试驱动开发(简称 TDD),有些人喜欢它,有些人则不喜欢。我们在这里不深入讨论,只是如果用 TDD,你得在写代码之前先写好测试。如果你好奇的话,可以去找 Wikipedia 上的文章看看。同时,我们也认为重构和测试可以很好地结合在一起。

测试 UI 部分通常很麻烦,因为它们包含太多活动部件。通常,view controller 需要和大量的 model 和 view 类交互。为了使 view controller 便于测试,我们要让任务尽量分离。

幸好,我们在更轻量的 view controller 这篇文章中的阐述的技术可以让测试更加简单。通常,如果你发现有些地方很难做测试,这就说明你的设计出了问题,你应该重构它。你可以重新参考更轻量的 view controller 这篇文章来获得一些帮助。总的目标就是有清晰的关注点分离。每个类只做一件事,并且做好。这样就可以让你只测试这件事。

记住:测试越多,回报的增长趋势越慢。首先你应该做简单的测试。当你觉得满意时,再加入更多复杂的测试。

Mocking

当你把一个整体拆分成小零件(比如更小的类)时,我们可以针对每个小的类来进行测试。但由于我们测试的类会和其他类交互,这里我们用一个所谓的 mock 或 stub 来绕开它。把 mock 对象看成是一个占位符,我们测试的类会跟这个占位符交互,而不是真正的那个对象。这样,我们就可以针对性地测试,并且保证不依赖于应用程序的其他部分。

在示例程序中,我们有个包含数组的 data source 需要测试。这个 data source 会在某个时候从 table view 中取出(dequeue)一个 cell。在测试过程中,还没有 table view,但是我们传递一个 mock 的 table view,这样即使没有 table view,也可以测试 data source,就像下面你即将看到的。起初可能有点难以理解,多看几次后,你就能体会到它的强大和简单。

Objective-C 中有个用来 mocking 的强大工具叫做 OCMock。它是一个非常成熟的项目,充分利用了 Objective-C 运行时强大的能力和灵活性。它使用了一些很酷的技巧,让通过 mock 对象来测试变得更加有趣。

本文后面有 data source 测试的例子,它更加详细地展示了这些技术如何工作在一起。

SenTestKit

编者注 这一节有一些过时了。在 Xcode 5 中 SenTestingKit 已经被 XCTest 完全取代,不过两者使用上没有太多区别,我们可以通过 Xcode 的 Edit -> Refactor -> Convert to XCTest 选项来切换到新的测试框架

我们将要使用的另一个工具是一个测试框架,开发者工具的一部分:Sente 的 SenTestingKit。这个上古神器从 1997 年起就伴随在 Objective-C 开发者左右,比第一款 iPhone 发布还早 10 年。现在,它已经集成到 Xcode 中了。SenTestingKit 会运行你的测试。通过 SenTestingKit,你将测试组织在类中。你需要给每一个你想测试的类创建一个测试类,类名以 Tests 结尾,它反应了这个类是干什么的。

这些测试类里的方法会做具体的测试工作。方法名必须以 test 开头来作为触发一个测试运行的条件。还有特殊的 -setUp 和 -tearDown 方法,你可以重载它们来设置各个测试。记住,你的测试类就是个类而已:只要对你有帮助,可以按需求在里面加 properties 和辅助方法。

做测试时,为测试类创建基类是个不错的模式。把通用的逻辑放到基类里面,可以让测试更简单和集中。可以通过示例程序中的例子来看看这样带来的好处。我们没有使用 Xcode 的测试模板,为了让事情简单有效,我们只创建了单独的 .m 文件。通过把类名改成以 Tests 结尾,类名可以反映出我们在对什么做测试。

编者注 Xcode 5 中 默认的测试模板也不再会自动创建 .h 文件了

与 Xcode 集成

测试会被 build 成一个 bundle,其中包含一个动态库和你选择的资源文件。如果你要测试某些资源文件,你得把它们加到测试的 target 中,Xcode 就会将它们打包到一个 bundle 中。接着你可以通过 NSBundle 来定位这些资源文件,示例项目实现了一个 -URLForResource:withExtension: 方法来方便的使用它。

Xcode 中的每个 scheme 定义了相应的测试 bundle 是哪个。通过 ⌘-R 运行程序,⌘-U 运行测试。

测试的运行依附于程序的运行,当程序运行时,测试 bundle 将被注入(injected)。测试时,你可能不想让你的程序做太多的事,那样会对测试造成干扰。可以把下面的代码加到 app delegate 中:

static BOOL isRunningTests(void) __attribute__((const));

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
if (isRunningTests()) {
return YES;
}

//
// Normal logic goes here
//

return YES;
}

static BOOL isRunningTests(void)
{
NSDictionary* environment = [[NSProcessInfo processInfo] environment];
NSString* injectBundle = environment[@"XCInjectBundle"];
return [[injectBundle pathExtension] isEqualToString:@"octest"];
}

编辑 Scheme 给了你极大的灵活性。你可以在测试之前或之后运行脚本,也可以有多个测试 bundle。这对大型项目来说很有用。最重要的是,可以打开或关闭个别测试,这对调试测试非常有用,只是要记得之后再把它们重新全部打开。

还要记住你可以为测试代码下断点,当测试执行时,调试器会在断点处停下来。

测试 Data Source
好了,让我们开始吧。我们已经通过拆分 view controller 让测试工作变得更轻松了。现在我们要测试 ArrayDataSource。首先我们新建一个空的,基本的测试类。我们把接口和实现都放到一个文件里;也没有哪个地方需要包含 @interface,放到一个文件会显得更加漂亮和整洁。

#import "PhotoDataTestCase.h"

@interface ArrayDataSourceTest : PhotoDataTestCase
@end

@implementation ArrayDataSourceTest
- (void)testNothing;
{
STAssertTrue(YES, @"");
}
@end

这个类没做什么事,只是展示了基本的设置。当我们运行这个测试时,-testNothing 方法将会运行。特别地,STAssert宏将会做琐碎的检查。注意,前缀 ST 源自于 SenTestingKit。这些宏和Xcode 集成,会把失败显示到侧边面板的Issues导航栏中。

第一个测试

我们现在把 testNothing 替换成一个简单、真正的测试:

- (void)testInitializing;
{
STAssertNil([[ArrayDataSource alloc] init], @"Should not be allowed.");
TableViewCellConfigureBlock block = ^(UITableViewCell *a, id b){};
id obj1 = [[ArrayDataSource alloc] initWithItems:@[]
cellIdentifier:@"foo"
configureCellBlock:block];
STAssertNotNil(obj1, @"");
}

接着,我们想测试ArrayDataSource实现的方法:

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

为此,我们创建一个测试方法:

- (void)testCellConfiguration;

首先,创建一个 data source:

__block UITableViewCell *configuredCell = nil;
__block id configuredObject = nil;
TableViewCellConfigureBlock block = ^(UITableViewCell *a, id b){
configuredCell = a;
configuredObject = b;
};
ArrayDataSource *dataSource = [[ArrayDataSource alloc] initWithItems:@[@"a", @"b"]
cellIdentifier:@"foo"
configureCellBlock:block];

注意,configureCellBlock 除了存储对象以外什么都没做,这可以让我们可以更简单地测试它。

然后,我们为 table view 创建一个 mock 对象:

id mockTableView = [OCMockObject mockForClass:[UITableView class]];

Data source 将在传进来的 table view 上调用 -dequeueReusableCellWithIdentifier:forIndexPath: 方法。我们将告诉 mock object 当它收到这个消息时要做什么。首先创建一个 cell,然后设置 mock。

UITableViewCell *cell = [[UITableViewCell alloc] init];
NSIndexPath* indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[[[mockTableView expect] andReturn:cell]
dequeueReusableCellWithIdentifier:@"foo"
forIndexPath:indexPath];

第一次看到它可能会觉得有点迷惑。我们在这里所做的,是让 mock 记录特定的调用。Mock 不是一个真正的 table view;我们只是假装它是。-expect 方法允许我们设置一个 mock,让它知道当这个方法调用时要做什么。

另外,-expect 方法也告诉 mock 这个调用必须发生。当我们稍后在 mock 上调用 -verify 时,如果那个方法没有被调用过,测试就会失败。相应地,-stub 方法也用来设置 mock 对象,但它不关心方法是否被调用过。

现在,我们要触发代码运行。我们就调用我们希望测试的方法。

NSIndexPath* indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
id result = [dataSource tableView:mockTableView
cellForRowAtIndexPath:indexPath];

然后我们测试是否一切正常:

STAssertEquals(result, cell, @"Should return the dummy cell.");
STAssertEquals(configuredCell, cell, @"This should have been passed to the block.");
STAssertEqualObjects(configuredObject, @"a", @"This should have been passed to the block.");
[mockTableView verify];

STAssert宏测试值的相等性。注意,前两个测试,我们通过比较指针来完成;我们不使用-isEqual:,是因为我们实际希望测试的是result,cell和configuredCell都是同一个对象。第三个测试要用 -isEqual:,最后我们调用 mock的 -verify方法。

注意,在示例程序中,我们是这样设置 mock 的:

id mockTableView = [self autoVerifiedMockForClass:[UITableView class]];

这是我们测试基类中的一个方便的封装,它会在测试最后自动调用 -verify 方法。

测试 UITableViewController
下面,我们转向PhotosViewController。它是个 UITableViewController 的子类,它使用了我们刚才测试过的 data source。View controller 剩下的代码已经相当简单了。

我们想测试点击 cell 后把我们带到详情页面,即一个 PhotoViewController的实例被 push 到 navigation controller 里面。我们再次使用 mocking 来让测试尽可能不依赖于其他部分。

首先我们创建一个 UINavigationController 的 mock:

id mockNavController = [OCMockObject mockForClass:[UINavigationController class]];

接下来,我们要使用部分 mocking。我们希望 PhotosViewController 实例的navigationController 返回 mockNavController。我们不能直接设置 navigation controller,所以我们简单地用 stub 来替换掉 PhotosViewController实例这个方法,让它返回mockNavController 就可以了。

PhotosViewController *photosViewController = [[PhotosViewController alloc] init];
id photosViewControllerMock = [OCMockObject partialMockForObject:photosViewController];
[[[photosViewControllerMock stub] andReturn:mockNavController] navigationController];

现在,任何时候对 photosViewController 调用 -navigationController 方法,都会返回 mockNavController。这是个强大的技巧,OCMock 就有这样的本领。

接下来,我们要告诉 navigation controller mock 我们调用的期望,即,一个 photo 不为 nil 的 detail view controller。

UIViewController* viewController = [OCMArg checkWithBlock:^BOOL(id obj) {
PhotoViewController *vc = obj;
return ([vc isKindOfClass:[PhotoViewController class]] &&
(vc.photo != nil));
}];
[[mockNavController expect] pushViewController:viewController animated:YES];

现在,我们触发 view 加载,并且模拟一行被点击:

UIView *view = photosViewController.view;
STAssertNotNil(view, @"");
NSIndexPath* indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[photosViewController tableView:photosViewController.tableView
didSelectRowAtIndexPath:indexPath];

最后我们验证 mocks 上期望的方法被调用过:

[mockNavController verify];
[photosViewControllerMock verify];

现在我们有了一个测试,用来测试和 navigation controller 的交互,以及正确 view controller 的创建。

又一次地,我们在示例程序中使用了便捷的方法:

- (id)autoVerifiedMockForClass:(Class)aClass;
- (id)autoVerifiedPartialMockForObject:(id)object;

于是,我们不需要记住调用-verify。

进一步探索

就像你从上面看到的那样,部分 mocking 非常强大。如果你看看 -[PhotosViewController setupTableView]方法的源码,你就会看到它是如何从 app delegate 中取出 model 对象的。

NSArray *photos = [AppDelegate sharedDelegate].store.sortedPhotos;

上面的测试依赖于这行代码。打破这种依赖的一种方式是再次使用 部分 mocking,让 app delegate 返回预定义的数据,就像这样:

id storeMock; // 假设我们已经设置过了
id appDelegate = [AppDelegate sharedDelegate]
id appDelegateMock = [OCMockObject partialMockForObject:appDelegate];
[[[appDelegateMock stub] andReturn:storeMock] store];

现在,无论何时调用[AppDelegate sharedDelegate].store,它将返回 storeMock。将这个技术使用好的话,可以确保让你的测试恰到好处地在保持简单和应对复杂之间找到平衡。

需要记住的事

部分 mock 技术将会在 mocks 的存在期间替换并保持被 mocking 的对象,并且一直有效。你可以通过提前调用[aMock stopMocking]来终于这种行为。大多数时候,你希望 部分 mock 在整个测试期间都保持有效。如果要提前终止,请确保在测试方法最后放置[aMock verify]。否则 ARC 会过早释放这个 mock,这样你就不能 -verify了,这不太可能是你想要的结果。

测试 NIB 加载

PhotoCell设置在一个 NIB 中,我们可以写一个简单的测试来检查 outlets 设置得是否正确。我们来回顾一下PhotoCell类:

@interface PhotoCell : UITableViewCell

+ (UINib *)nib;

@property (weak, nonatomic) IBOutlet UILabel* photoTitleLabel;
@property (weak, nonatomic) IBOutlet UILabel* photoDateLabel;

@end

我们的简单测试的实现看上去是这样:

@implementation PhotoCellTests

- (void)testNibLoading;
{
UINib *nib = [PhotoCell nib];
STAssertNotNil(nib, @"");

NSArray *a = [nib instantiateWithOwner:nil options:@{}];
STAssertEquals([a count], (NSUInteger) 1, @"");
PhotoCell *cell = a[0];
STAssertTrue([cell isMemberOfClass:[PhotoCell class]], @"");

// 检查 outlet 是否正确设置
STAssertNotNil(cell.photoTitleLabel, @"");
STAssertNotNil(cell.photoDateLabel, @"");
}

@end

非常基础,但是能出色完成工作。

值得一提的是,当有发生改变时,我们需要同时更新测试以及相应的类或 nib 。这是事实。你需要考虑改变类或者 nib 文件时可能会打破原有的 outlets 连接。如果你用了 .xib 文件,你可能要注意了,这是经常发生的事。

关于 Class 和 Injection
我们已经从与 Xcode 集成得知,测试 bundle 会注入到应用程序中。省略注入的如何工作的细节(它本身是个巨大的话题),简单地说:注入是把待注入的 bundle(我们的测试 bundle)中的 Objective-C 类添加到运行的应用程序中。这很好,因为这样允许我们运行测试了。

还有一件事会很让人迷惑,那就是如果我们同时把一个类添加到应用程序和测试 bundle中。如果在上面的示例程序中,我们(不小心)把 PhotoCell 类同时添加到测试 bundle 和应用程序里的话,在测试 bundle 中调用 [PhotoCell class]会返回一个不同的指针(你应用程序中的那个类)。于是我们的测试将会失败:

STAssertTrue([cell isMemberOfClass:[PhotoCell class]], @"");

再一次声明:注入很复杂。你应该确认的是:不要把应用程序中的 .m 文件添加到测试 target 中。否则你会得到预想不到的行为。

额外的思考

如果你使用一个持续集成 (CI) 的解决方案,让你的测试启动和运行是一个好主意。详细的描述超过了本文的范围。这些脚本通过 RunUnitTests 脚本触发。还有个 TEST_AFTER_BUILD 环境变量。

另一种有趣的选择是创建单独的测试 bundle 来自动化性能测试。你可以在测试方法里做任何你想做的。定时调用一些方法并使用 STAssert 来检查它们是否在特定阈值里面是其中一种选择。

链接:https://www.jianshu.com/p/733fa8bbca95

收起阅读 »

基于环信MQTT消息云,Java版MQTT客户端快速实现消息收发

本文介绍Java版MQTT 客户端,如何连接环信MQTT消息云快速实现消息的自收自发。 一、前提条件1.部署Java开发环境安装IDE。您可以使用IntelliJ IDEA或者Eclipse,本文以IntelliJ IDEA为例。下载安装JDK。&n...
继续阅读 »

本文介绍Java版MQTT 客户端,如何连接环信MQTT消息云快速实现消息的自收自发。

 

一、前提条件

1.部署Java开发环境

安装IDE。您可以使用IntelliJ IDEA或者Eclipse,本文以IntelliJ IDEA为例。
下载安装
JDK 

2.导入项目依赖

IntelliJ IDEA中创建工程,并确认pom.xml中包含以下依赖。


commons-codec
commons-codec
1.10


org.eclipse.paho
org.eclipse.paho.client.mqttv3
1.2.2


org.apache.httpcomponents
httpclient
4.5.2


com.alibaba
fastjson
1.2.76

二、实现流程

1、获取鉴权

     为保障客户安全性需求,环信MQTT消息云服务为客户提供【token+clientID】方式实现鉴权认证,其中AppID(clientID中的关键字段)及token标识获取流程如下:

【登录console】
     欢迎您登录环信云console控制台,在此控制台中,为您提供应用列表、解决方案、DEMO体验以及常见问题等功能。
     在应用列表中,若您未在APP中开通MQTT业务,可参见APP  MQTT开通流程
     若APP已开通MQTT业务,可在应用列表中选中Appname,点击【查看】操作,进入应用详情。


【获取AppID及连接地址】 
      进入【查看】后,点击左侧菜单栏【MQTT】->【服务概览】,在下图红色方框内获取当前AppID及服务器连接地址。

【获取token】
     为实现对用户管控及接入安全性,环信云console提供用户认证功能,支持对用户账户的增、删、改、查以及为每个用户账户分配唯一token标识,获取token标识可选择以下两种形式。
  形式一:console控制台获取(管理员视角)
  * 点击左侧菜单栏【应用概览】->【用户认证】页面,点击【创建IM用户】按钮,增添新的账户信息(包  括用户名及密码)。
  * 创建成功后,在【用户ID】列表中选中账户,点击【查看token】按钮获取当前账户token信息。


  形式二:客户端代码获取(客户端视角)
  * 获取域名:点击左侧菜单栏【即时通讯】->【服务概览】页面,查看下图中token域名、org_name、app_name。


  * 拼接URL:获取token URL格式为:http:/ /token域名/org_name/app_name/token。 
  * 用户名/密码:使用【用户ID】列表中已有账户的用户名及密码,例“用户名:test/密码:test123”。

客户端获取token代码示例如下:

public static void main() 
{
// 获取token的URL
http://{token域名}/{org_name}/{app_name}/token
// 获取token
String token = "";
// 取token
try (final CloseableHttpClient httpClient = HttpClients.createDefault())
{
final HttpPost httpPost = new HttpPost("http://{token域名}/{org_name}/{app_name}/token");
Map params = new HashMap<>();
params.put("grant_type", "password");
params.put("username", "test");
params.put("password", "test123");
//设置请求体参数
StringEntity entity = new StringEntity(JSONObject.toJSONString(params), Charset.forName("utf-8"));
entity.setContentEncoding("utf-8");
httpPost.setEntity(entity);
//设置请求头部
httpPost.setHeader("Content-Type", "application/json");
//执行请求,返回请求响应
try (final CloseableHttpResponse response = httpClient.execute(httpPost)
{
//请求返回状态码
int statusCode = response.getStatusLine().getStatusCode();
//请求成功
if (statusCode == HttpStatus.SC_OK && statusCode <= HttpStatus.SC_TEMPORARY_REDIRECT)
{
//取出响应体
final HttpEntity entity2 = response.getEntity();
//从响应体中解析出token
String responseBody = EntityUtils.toString(entity2, "utf-8");
JSONObject jsonObject = JSONObject.parseObject(responseBody);
token = jsonObject.getString("access_token");
}
else
{
//请求失败
throw new ClientProtocolException("请求失败,响应码为:" + statusCode);
}
}
}
catch (IOException e)
{
e.printStackTrace();
}
}

返回结果

 {
"access_token": "YWMtN8a0oqV3EeuF0AmiqRgEh-grzF8zZk2Wp8GS3pF-orDW_F-gj3kR6os3h_oz3ROQAwMAAAF5BxhGlwBPGgAvTR8vDrdVsDPNZMQj0fFjv7EaohgZhzMHM9ncVLE30g",
"expires_in": 5184000,
"user":
{
"uuid": "d6fc5fa0-8f79-11ea-8b37-87fa33dd1390",
"type": "user",
"created": 1588756404898,
"modified": 1588756404898,
"username": "test",
"activated": true
}
}
access_token即为要获取的token

2、初始化

      在IntelliJ IDEA工程中创建MQTT客户端,客户端初始配置包括创建clientID,topic名称,QoS质量,连接地址等信息。

import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import static org.eclipse.paho.client.mqttv3.MqttConnectOptions.MQTT_VERSION_3_1_1;
public class MqttDemoStarter 
{
public static void main(String[] args) throws MqttException, InterruptedException {
/**
* 用户指定
* /
String deviceId = "xxxxx-xxxx-xxxxx-xxxxx-xxxxx";
/**
* 从console控制台获取
* /
String appId = "1NQ1E9";
/**
* 设置接入点,进入console控制台获取
*/
String endpoint = "1NQ1E9.sandbox.mqtt.chat";
/**
* MQTT客户端ID,由业务系统分配,需要保证每个TCP连接都不一样,保证全局唯一,如果不同的客户端对象(TCP连接)使用了相同的clientId会导致之前的连接断开。
* clientId由两部分组成,格式为DeviceID@appId,其中DeviceID由业务方自己设置,appId在console控制台创建,clientId总长度不得超过64个字符。
*/
String clientId = deviceId + "@" + appId;

/**
* 需要订阅或发送消息的topic名称
* 如果使用了没有创建或者没有被授权的Topic会导致鉴权失败,服务端会断开客户端连接。
*/
final String myTopic = "myTopic";

/**
* QoS参数代表传输质量,可选0,1,2。详细信息,请参见名词解释。
*/
final int qosLevel = 0;
final MemoryPersistence memoryPersistence = new MemoryPersistence();

/**
* 客户端协议和端口。客户端使用的协议和端口必须匹配,如果是ws或者wss,使用http://;如果是mqtt或者mqtts,使用tcp://
*/
final MqttClient mqttClient = new MqttClient("tcp://" + endpoint + ":1883", clientId, memoryPersistence);
/**
* 设置客户端发送超时时间,防止无限阻塞。
*/
mqttClient.setTimeToWait(5000);

final ExecutorService executorService = new ThreadPoolExecutor(1, 1, 0, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>());
}

3、连接服务器

    配置连接密码、cleansession标志、心跳间隔、超时时间等信息,调用connect()函数连接至环信MQTT消息云。

MqttConnectOptions mqttConnectOptions = new MqttConnectOptions();
/**
* 用户名,在console中注册
*/
mqttConnectOptions.setUserName("test");
/**
* 用户密码为第一步中申请的token
*/
mqttConnectOptions.setPassword(token.toCharArray());
mqttConnectOptions.setCleanSession(true);
mqttConnectOptions.setKeepAliveInterval(90);
mqttConnectOptions.setAutomaticReconnect(true);
mqttConnectOptions.setMqttVersion(MQTT_VERSION_3_1_1);
mqttConnectOptions.setConnectionTimeout(5000);
mqttClient.connect(mqttConnectOptions);
//暂停1秒钟,等待连接订阅完成
Thread.sleep(1000);

4、订阅【subscribe】

【订阅主题】

    当客户端成功连接环信MQTT消息云后,需尽快向服务器发送订阅主题消息。

mqttClient.setCallback(new MqttCallbackExtended() {
/**
* 连接完成回调方法
* @param b
* @param s
*/
@Override
public void connectComplete(boolean b, String s) {
/**
* 客户端连接成功后就需要尽快订阅需要的Topic。
*/
System.out.println("connect success");
executorService.submit(() -> {
try {
final String[] topicFilter = {myTopic};
final int[] qos = {qosLevel};
mqttClient.subscribe(topicFilter, qos);
} catch (Exception e) {
e.printStackTrace();
}
});
}
});

【取消订阅】

mqttClient.unsubscribe(new String[]{myTopic});

【接收消息】

    配置接收消息回调方法,从环信MQTT消息云接收订阅消息。

mqttClient.setCallback(new MqttCallbackExtended() {
/**
* 接收消息回调方法
* @param s
* @param mqttMessage
*/
@Override
public void messageArrived(String s, MqttMessage mqttMessage) {
System.out.println("receive msg from topic " + s + " , body is " + new String(mqttMessage.getPayload()));
}
});

5、发布【publish】

    配置发送消息回调方法,向环信MQTT消息云中指定topic发送消息。

for (int i = 0; i < 10; i++) {
/**
* 构建一个Mqtt消息
*/
MqttMessage message = new MqttMessage("hello world pub sub msg".getBytes());
//设置传输质量
message.setQos(qosLevel);
/**
* 发送普通消息时,Topic必须和接收方订阅的Topic一致,或者符合通配符匹配规则。
*/
mqttClient.publish(myTopic, message);
}

6、结果验证

connect success
send msg succeed topic is : myTopic
send msg succeed topic is : myTopic
send msg succeed topic is : myTopic
send msg succeed topic is : myTopic
send msg succeed topic is : myTopic
send msg succeed topic is : myTopic
send msg succeed topic is : myTopic
send msg succeed topic is : myTopic
send msg succeed topic is : myTopic
send msg succeed topic is : myTopic
receive msg from topic myTopic , body is hello world pub sub msg
receive msg from topic myTopic , body is hello world pub sub msg
receive msg from topic myTopic , body is hello world pub sub msg
receive msg from topic myTopic , body is hello world pub sub msg
receive msg from topic myTopic , body is hello world pub sub msg
receive msg from topic myTopic , body is hello world pub sub msg
receive msg from topic myTopic , body is hello world pub sub msg
receive msg from topic myTopic , body is hello world pub sub msg
receive msg from topic myTopic , body is hello world pub sub msg
receive msg from topic myTopic , body is hello world pub sub msg

三、更多信息

  * 完整demo示例,请参见demo下载

或直接下载:MQTTChatDemo- Java.zip

  * 目前MQTT客户端支持多种语言,请参见 SDK下载
  * 如果您在使用环信MQTT消息云服务中,有任何疑问和建议,欢迎您联系我们

 

收起阅读 »

一个围绕 CFNetwork API的网络通讯库,断点续传神器!

ASIHTTPRequest 是一个围绕 CFNetwork API的易于使用的包装器,它使与 Web 服务器通信的一些更乏味的方面变得更容易。它是用 Objective-C 编写的,适用于 Mac OS X 和 iPhone 应用程序。它适用于执行...
继续阅读 »

ASIHTTPRequest 是一个围绕 CFNetwork API的易于使用的包装器,它使与 Web 服务器通信的一些更乏味的方面变得更容易。它是用 Objective-C 编写的,适用于 Mac OS X 和 iPhone 应用程序。

它适用于执行基本的HTTP请求并与基于REST的服务(GET / POST / PUT / DELETE交互包含的 ASIFormDataRequest 子类使得使用 multipart/form-data提交POST数据和文件变得容易

它提供:

  • 一个简单的界面,用于向网络服务器提交数据和从网络服务器获取数据
  • 将数据下载到内存或直接下载到磁盘上的文件
  • 在本地驱动器上提交文件作为POST数据的一部分,与HTML文件输入机制兼容
  • 将请求正文直接从磁盘传输到服务器,以节省内存
  • 恢复部分下载
  • 轻松访问请求和响应HTTP标头
  • 进度委托(NSProgressIndicators 和 UIProgressViews)显示有关下载上传进度的信息
  • 操作队列上传下载进度指示器的自动魔法管理
  • 基本、摘要 + NTLM身份验证支持,凭据在会话期间自动重复使用,并且可以存储在钥匙串中以备日后使用。
  • 饼干支持
  • []当您的应用程序移至后台时,请求可以继续运行(iOS 4+)
  • GZIP支持响应数据请求正文
  • 包含的 ASIDownloadCache 类允许请求透明地缓存响应,并且即使在没有可用网络的情况下也允许对缓存数据的请求成功
  • [] ASIWebPageRequest – 下载完整的网页,包括图像和样式表等外部资源。即使没有网络连接,任何大小的页面都可以无限期地缓存,并显示在 UIWebview/WebView 中。
  • 易于使用的 Amazon S3 支持 – 无需自己动手签署请求!
  • 完全支持 Rackspace 云文件
  • []客户端证书支持
  • 支持手动和自动检测代理、验证代理和PAC文件自动配置。内置的登录对话框让您的 iPhone 应用程序可以透明地使用身份验证代理,无需任何额外工作。
  • 带宽限制支持
  • 支持持久连接
  • 支持同步和异步请求
  • 通过委托或 [ NEW ] 块获取有关请求状态更改的通知(Mac OS X 10.6、iOS 4 及更高版本)
  • 带有广泛的单元测试

ASIHTTPRequest 兼容 Mac OS 10.5 或更高版本,以及 iOS 3.0 或更高版本。


创建同步请求

使用 ASIHTTPRequest 的最简单方法。发送startSynchronous 消息将在同一线程中执行请求,并在完成(成功或失败)时返回控制权。

通过检查error属性来检查问题

要以字符串形式获取响应,请调用responseString方法。不要将它用于二进制数据 - 使用responseData获取 NSData 对象,或者,对于较大的文件,将您的请求设置为使用downloadDestinationPath属性下载到文件

- (IBAction)grabURL:(id)sender
{
NSURL *url = [NSURL URLWithString:@"http://allseeing-i.com"];
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request startSynchronous];
NSError *error = [request error];
if (!error) {
NSString *response = [request responseString];
}
}

与前面的示例执行相同的操作,但请求在后台运行。

- (IBAction)grabURLInBackground:(id)sender
{
NSURL *url = [NSURL URLWithString:@"http://allseeing-i.com"];
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setDelegate:self];
[request startAsynchronous];
}
 
- (void)requestFinished:(ASIHTTPRequest *)request
{
// Use when fetching text data
NSString *responseString = [request responseString];
 
// Use when fetching binary data
NSData *responseData = [request responseData];
}
 
- (void)requestFailed:(ASIHTTPRequest *)request
{
NSError *error = [request error];
}

请注意,我们设置了请求的委托属性,以便我们可以在请求完成或失败时收到通知。

这是创建异步请求的最简单方法,它将在后台运行在全局 NSOperationQueue 中。对于更复杂的操作(例如跟踪多个请求的进度),您可能希望创建自己的队列,这就是我们接下来将介绍的内容。

使用块

从 v1.8 开始,我们可以在支持块的平台上使用块来做同样的事情:

- (IBAction)grabURLInBackground:(id)sender
{
NSURL *url = [NSURL URLWithString:@"http://allseeing-i.com"];
__block ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setCompletionBlock:^{
// Use when fetching text data
NSString *responseString = [request responseString];
 
// Use when fetching binary data
NSData *responseData = [request responseData];
}];
[request setFailedBlock:^{
NSError *error = [request error];
}];
[request startAsynchronous];
}

请注意在我们声明请求时使用__block限定符,这很重要!它告诉块不要保留请求,这对于防止保留循环很重要,因为请求将始终保留块。

使用队列

这个例子再次做同样的事情,但我们为我们的请求创建了一个 NSOperationQueue。

使用您自己创建的 NSOperationQueue(或 ASINetworkQueue,见下文)可以让您更好地控制异步请求。使用队列时,只能同时运行一定数量的请求。如果您添加的请求多于队列的maxConcurrentOperationCount属性,则请求将在开始之前等待其他人完成。

- (IBAction)grabURLInTheBackground:(id)sender
{
if (![self queue]) {
[self setQueue:[[[NSOperationQueue alloc] init] autorelease]];
}
 
NSURL *url = [NSURL URLWithString:@"http://allseeing-i.com"];
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setDelegate:self];
[request setDidFinishSelector:@selector(requestDone:)];
[request setDidFailSelector:@selector(requestWentWrong:)];
[[self queue] addOperation:request]; //queue is an NSOperationQueue
}
 
- (void)requestDone:(ASIHTTPRequest *)request
{
NSString *response = [request responseString];
}
 
- (void)requestWentWrong:(ASIHTTPRequest *)request
{
NSError *error = [request error];
}


demo及常见问题:https://github.com/paytronix/ASIHTTPRequest

源码下载:ASIHTTPRequest-master.zip


收起阅读 »

扁平化 UI 的 iOS 组件-FlatUIKit

FlatUIKitFlatUIKit 是我们在为 iPhone构建Grouper 时创建的具有“Flat UI”美学风格的 iOS 组件集合。它的设计灵感来自于Flat UI和Kyle Miller。样式是通过替换现有 UIKit 组件的类别来实现的,因此将其...
继续阅读 »


FlatUIKit

FlatUIKit 是我们在为 iPhone构建Grouper 时创建的具有“Flat UI”美学风格的 iOS 组件集合它的设计灵感来自于Flat UIKyle Miller样式是通过替换现有 UIKit 组件的类别来实现的,因此将其集成到您的项目中非常简单。

安装

FlatUIKit 可以通过CocoaPods安装只需添加

pod 'FlatUIKit'

组件

FUIButton 是 UIButton 的一个嵌入式子类,它公开了额外的属性 buttonColor、shadowColor、cornerRadius 和 shadowHeight。请注意,如果您设置了其中任何一个,则必须设置所有这些。

myButton.buttonColor = [UIColor turquoiseColor ];
myButton.shadowColor = [UIColor
greenSeaColor ];
myButton.shadowHeight =
30f ;
myButton.cornerRadius =
60f ;
myButton.titleLabel.font = [UIFont
boldFlatFontOfSize: 16 ];
[myButton
setTitleColor: [UIColor cloudColor ] forState: UIControlStateNormal];
[myButton
setTitleColor: [UIColor cloudColor ] forState: UIControlStateHighlighted];

FUITextField 是 UITextField 的一个嵌入式子类,它公开了附加属性 edgeInsets、textFieldColor、borderColor、borderWidth 和 cornerRadius。请注意,如果您设置了其中任何一个,则必须设置所有这些。

myTextField.font = [UIFont flatFontOfSize: 16 ];
myTextField.backgroundColor = [UIColor
clearColor ];
myTextField.edgeInsets = UIEdgeInsetsMake(
4 . 0f , 15 . 0f , 4 . 0f , 15 . 0f );
myTextField.textFieldColor = [UIColor
whiteColor ];
myTextField.borderColor = [UIColor
turquoiseColor ];
myTextField.borderWidth =
20f ;
myTextField.cornerRadius =
30f ;

FUISegmentedControl 是 UISegmentedControl 的一个嵌入式子类,它公开了附加属性 selectedColor、deselectedColor、selectedFont、deselectedFont、selectedFontColor、deselectedFontColor、dividerColor 和 cornerRadius。请注意,如果您设置了其中任何一个,建议您设置所有这些。

mySegmentedControl.selectedFont = [UIFont boldFlatFontOfSize: 16 ];
mySegmentedControl.selectedFontColor = [UIColor
cloudColor ];
mySegmentedControl.deselectedFont = [UIFont
flatFontOfSize: 16 ];
mySegmentedControl.deselectedFontColor = [UIColor
cloudColor ];
mySegmentedControl.selectedColor = [UIColor
amethystColor ];
mySegmentedControl.deselectedColor = [UIColor
silverColor ];
mySegmentedControl.dividerColor = [的UIColor
midnightBlueColor ];
mySegmentedControl.cornerRadius =
5.0 ;

FUISwitch 不是 UISwitch 的子类(UISwitch 的子类太不灵活了),而是一个重新实现,它暴露了 UISwitch 的所有方法。此外,它还提供对其底层开/关 UILabels 和其他子视图的访问。

mySwitch.onColor = [UIColor turquoiseColor ];
mySwitch.offColor = [UIColor
cloudColor ];
mySwitch.onBackgroundColor = [的UIColor
midnightBlueColor ];
mySwitch.offBackgroundColor = [UIColor
silverColor ];
mySwitch.offLabel.font = [UIFont
boldFlatFontOfSize: 14 ];
mySwitch.onLabel.font = [UIFont
boldFlatFontOfSize: 14 ];

与 FUISwitch 类似,FUIAlertView 是 UIAlertView 的重新实现,它公开了 UIAlertView 的所有方法(和委托方法,使用 FUIAlertViewDelegate 协议),但在 UI 定制方面具有更大的灵活性。它的所有子 UILabels、UIViews 和 FUIButtons 都可以随意定制。

FUIAlertView *alertView = [[FUIAlertView alloc ] initWithTitle: @" Hello "
message: @" This is an alert view "
delegate: nil cancelButtonTitle: @" Dismiss "
otherButtonTitles: @" Do Something " , nil ];
alertView.titleLabel.textColor = [UIColor
cloudColor ];
alertView.titleLabel.font = [UIFont
boldFlatFontOfSize: 16 ];
alertView.messageLabel.textColor = [UIColor
cloudColor ];
alertView.messageLabel.font = [UIFont
flatFontOfSize: 14 ];
alertView.backgroundOverlay.backgroundColor = [[UIColor
cloudColor ] colorWithAlphaComponent: 0.8 ];
alertView.alertContainer.backgroundColor = [的UIColor
midnightBlueColor ];
alertView.defaultButtonColor = [UIColor
cloudColor ];
alertView.defaultButtonShadowColor = [UIColor
asbestosColor ];
alertView.defaultButtonFont = [UIFont
boldFlatFontOfSize: 16 ];
alertView.defaultButtonTitleColor = [UIColor
asbestosColor ];

为了提供平面 UISlider、UIProgressViews 和 UISteppers,我们只需在 UISlider/ProgressView/UIStepper 上提供类别,以使用适当的颜色/角半径自动配置它们的外观。这有助于与您现有的项目零摩擦集成:

[mySlider configureFlatSliderWithTrackColor: [UIColor silverColor ]
progressColor: [UIColor alizarinColor ]
thumbColor: [UIColor pomegranateColor ]];

FUIS滑块

[myProgressView configureFlatProgressViewWithTrackColor: [UIColor silverColor ]
progressColor: [UIColor alizarinColor ]];

[myStepper
configureFlatStepperWithColor: [的UIColor wisteriaColor ]
highlightedColor: [的UIColor wisteriaColor ]
disabledColor: [的UIColor amethystColor ]
iconColor通过: [的UIColor cloudsColor ]];

要为整个应用程序自定义栏按钮项(包括后退按钮),UIBarButtonItem+FlatUI 提供了一个类方法,该方法利用 UIBarButtonItem 外观代理一步完成此操作:

[UIBarButtonItem configureFlatButtonsWithColor: [UIColor peterRiverColor ]
highlightColor: [UIColor belizeHoleColor ]
cornerRadius: 3 ];

但是,这可能会导致从操作表、共享表或 web 视图中的链接推送的控制器出现渲染问题。为防止这种行为,请将自定义栏按钮项的范围限定到您的控制器:

[UIBarButtonItem configureFlatButtonsWithColor: [UIColor peterRiverColor ]
highlightColor: [UIColor belizeHoleColor ]
cornerRadius: 3
whenContainedIn: [YourViewController class ]];

您可以修改 UITableViewCell 的 backgroundColor 和 selectedBackgroundColor 而不会丢失圆角。单元格将复制 UITableView 的分隔符颜色。分隔符高度显示为 separatorHeight,半径显示为 cornerRadius。

UITableViewCell *cell = ...;
[cell
configureFlatCellWithColor: [UIColor greenSeaColor ]
selectedColor: [UIColor cloudColor ]
roundingCorners: corners];

cell.cornerRadius =
50f ; //可选
cell.separatorHeight =
2 . 0f ; //可选

demo下载及常见问题:https://github.com/Grouper/FlatUIKit
源码下载:FlatUIKit.zip


收起阅读 »

模型处理工具不仅仅只有YYModel,还有更强的Mantle

Mantle 使为您的 Cocoa 或 Cocoa Touch 应用程序编写简单的模型层变得容易Let's use the GitHub API for demonstration~!typedef enum : NSUInteger { ...
继续阅读 »

Mantle 使为您的 Cocoa 或 Cocoa Touch 应用程序编写简单的模型层变得容易

Let's use the GitHub API for demonstration~!

typedef enum : NSUInteger {
GHIssueStateOpen,
GHIssueStateClosed
} GHIssueState;

@interface GHIssue : NSObject <NSCoding, NSCopying>

@property (nonatomic, copy, readonly) NSURL *URL;
@property (nonatomic, copy, readonly) NSURL *HTMLURL;
@property (nonatomic, copy, readonly) NSNumber *number;
@property (nonatomic, assign, readonly) GHIssueState state;
@property (nonatomic, copy, readonly) NSString *reporterLogin;
@property (nonatomic, copy, readonly) NSDate *updatedAt;
@property (nonatomic, strong, readonly) GHUser *assignee;
@property (nonatomic, copy, readonly) NSDate *retrievedAt;

@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *body;

- (id)initWithDictionary:(NSDictionary *)dictionary;

@end


typedef enum : NSUInteger {
GHIssueStateOpen,
GHIssueStateClosed
} GHIssueState;

@interface GHIssue : NSObject <NSCoding, NSCopying>

@property (nonatomic, copy, readonly) NSURL *URL;
@property (nonatomic, copy, readonly) NSURL *HTMLURL;
@property (nonatomic, copy, readonly) NSNumber *number;
@property (nonatomic, assign, readonly) GHIssueState state;
@property (nonatomic, copy, readonly) NSString *reporterLogin;
@property (nonatomic, copy, readonly) NSDate *updatedAt;
@property (nonatomic, strong, readonly) GHUser *assignee;
@property (nonatomic, copy, readonly) NSDate *retrievedAt;

@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *body;

- (id)initWithDictionary:(NSDictionary *)dictionary;

@end
@implementation GHIssue

+ (NSDateFormatter *)dateFormatter {
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'";
return dateFormatter;
}

- (id)initWithDictionary:(NSDictionary *)dictionary {
self = [self init];
if (self == nil) return nil;

_URL = [NSURL URLWithString:dictionary[@"url"]];
_HTMLURL = [NSURL URLWithString:dictionary[@"html_url"]];
_number = dictionary[@"number"];

if ([dictionary[@"state"] isEqualToString:@"open"]) {
_state = GHIssueStateOpen;
} else if ([dictionary[@"state"] isEqualToString:@"closed"]) {
_state = GHIssueStateClosed;
}

_title = [dictionary[@"title"] copy];
_retrievedAt = [NSDate date];
_body = [dictionary[@"body"] copy];
_reporterLogin = [dictionary[@"user"][@"login"] copy];
_assignee = [[GHUser alloc] initWithDictionary:dictionary[@"assignee"]];

_updatedAt = [self.class.dateFormatter dateFromString:dictionary[@"updated_at"]];

return self;
}

- (id)initWithCoder:(NSCoder *)coder {
self = [self init];
if (self == nil) return nil;

_URL = [coder decodeObjectForKey:@"URL"];
_HTMLURL = [coder decodeObjectForKey:@"HTMLURL"];
_number = [coder decodeObjectForKey:@"number"];
_state = [coder decodeUnsignedIntegerForKey:@"state"];
_title = [coder decodeObjectForKey:@"title"];
_retrievedAt = [NSDate date];
_body = [coder decodeObjectForKey:@"body"];
_reporterLogin = [coder decodeObjectForKey:@"reporterLogin"];
_assignee = [coder decodeObjectForKey:@"assignee"];
_updatedAt = [coder decodeObjectForKey:@"updatedAt"];

return self;
}

- (void)encodeWithCoder:(NSCoder *)coder {
if (self.URL != nil) [coder encodeObject:self.URL forKey:@"URL"];
if (self.HTMLURL != nil) [coder encodeObject:self.HTMLURL forKey:@"HTMLURL"];
if (self.number != nil) [coder encodeObject:self.number forKey:@"number"];
if (self.title != nil) [coder encodeObject:self.title forKey:@"title"];
if (self.body != nil) [coder encodeObject:self.body forKey:@"body"];
if (self.reporterLogin != nil) [coder encodeObject:self.reporterLogin forKey:@"reporterLogin"];
if (self.assignee != nil) [coder encodeObject:self.assignee forKey:@"assignee"];
if (self.updatedAt != nil) [coder encodeObject:self.updatedAt forKey:@"updatedAt"];

[coder encodeUnsignedInteger:self.state forKey:@"state"];
}

- (id)copyWithZone:(NSZone *)zone {
GHIssue *issue = [[self.class allocWithZone:zone] init];
issue->_URL = self.URL;
issue->_HTMLURL = self.HTMLURL;
issue->_number = self.number;
issue->_state = self.state;
issue->_reporterLogin = self.reporterLogin;
issue->_assignee = self.assignee;
issue->_updatedAt = self.updatedAt;

issue.title = self.title;
issue->_retrievedAt = [NSDate date];
issue.body = self.body;

return issue;
}

- (NSUInteger)hash {
return self.number.hash;
}

- (BOOL)isEqual:(GHIssue *)issue {
if (![issue isKindOfClass:GHIssue.class]) return NO;

return [self.number isEqual:issue.number] && [self.title isEqual:issue.title] && [self.body isEqual:issue.body];
}

@end

哇,对于这么简单的事情来说,这是很多样板!而且,即便如此,这个例子也没有解决一些问题:

  • 无法GHIssue使用来自服务器的新数据更新 a 
  • 没有办法将 a 变GHIssue JSON。
  • GHIssueState不应按原样编码。如果将来枚举更改,现有存档可能会中断。
  • 如果未来的界面发生GHIssue变化,现有的档案可能会中断。


为什么不使用核心数据?

Core Data 很好地解决了某些问题。如果您需要对数据执行复杂的查询,处理具有大量关系的巨大对象图,或支持撤消和重做,Core Data 非常适合。

然而,它确实有几个痛点:

  • 仍然有很多样板。托管对象减少了上面看到的一些样板,但 Core Data 有很多自己的样板。正确设置 Core Data 堆栈(带有持久存储和持久存储协调器)并执行提取可能需要多行代码。
  • 很难做对。即使是有经验的开发人员在使用 Core Data 时也可能会犯错误,而且该框架是不可原谅的。

如果您只是想访问一些 JSON 对象,那么 Core Data 可能会做大量工作,但收获甚微。

尽管如此,如果您已经在应用程序中使用或想要使用 Core Data,Mantle 仍然可以作为 API 和托管模型对象之间的方便转换层。


MTL模型

输入 MTLModelGHIssue看起来像继承自MTLModel

typedef enum : NSUInteger {
GHIssueStateOpen,
GHIssueStateClosed
} GHIssueState;

@interface GHIssue : MTLModel

@property (nonatomic, copy, readonly) NSURL *URL;
@property (nonatomic, copy, readonly) NSURL *HTMLURL;
@property (nonatomic, copy, readonly) NSNumber *number;
@property (nonatomic, assign, readonly) GHIssueState state;
@property (nonatomic, copy, readonly) NSString *reporterLogin;
@property (nonatomic, strong, readonly) GHUser *assignee;
@property (nonatomic, copy, readonly) NSDate *updatedAt;

@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *body;

@property (nonatomic, copy, readonly) NSDate *retrievedAt;

@end


@implementation GHIssue

+ (NSDateFormatter *)dateFormatter {
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'";
return dateFormatter;
}

+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{
@"URL": @"url",
@"HTMLURL": @"html_url",
@"number": @"number",
@"state": @"state",
@"reporterLogin": @"user.login",
@"assignee": @"assignee",
@"updatedAt": @"updated_at"
};
}

+ (NSValueTransformer *)URLJSONTransformer {
return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
}

+ (NSValueTransformer *)HTMLURLJSONTransformer {
return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
}

+ (NSValueTransformer *)stateJSONTransformer {
return [NSValueTransformer mtl_valueMappingTransformerWithDictionary:@{
@"open": @(GHIssueStateOpen),
@"closed": @(GHIssueStateClosed)
}];
}

+ (NSValueTransformer *)assigneeJSONTransformer {
return [MTLJSONAdapter dictionaryTransformerWithModelClass:GHUser.class];
}

+ (NSValueTransformer *)updatedAtJSONTransformer {
return [MTLValueTransformer transformerUsingForwardBlock:^id(NSString *dateString, BOOL *success, NSError *__autoreleasing *error) {
return [self.dateFormatter dateFromString:dateString];
} reverseBlock:^id(NSDate *date, BOOL *success, NSError *__autoreleasing *error) {
return [self.dateFormatter stringFromDate:date];
}];
}

- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error {
self = [super initWithDictionary:dictionaryValue error:error];
if (self == nil) return nil;

// Store a value that needs to be determined locally upon initialization.
_retrievedAt = [NSDate date];

return self;
}

@end


值得注意的是,从这个版本是缺席的实现, -isEqual:,和-hash通过检查@property 您在子类中声明,MTLModel可以为所有这些方法提供默认实现。

原始示例的问题也都被修复了:

无法GHIssue使用来自服务器的新数据更新 a 

MTLModel有一个可扩展的-mergeValuesForKeysFromModel:方法,可以很容易地指定应该如何集成新的模型数据。

没有办法将 a 变GHIssue JSON。

这是可逆变压器真正派上用场的地方。+[MTLJSONAdapter JSONDictionaryFromModel:error:]可以将任何符合 的模型对象转换 回 JSON 字典。+[MTLJSONAdapter JSONArrayFromModels:error:]是相同的,但将模型对象数组转换为字典的 JSON 数组。

如果未来的界面发生GHIssue变化,现有的档案可能会中断。

MTLModel自动保存用于存档的模型对象的版本。取消归档时,-decodeValueForKey:withCoder:modelVersion:如果被覆盖将被调用,为您提供一个方便的挂钩来升级旧数据。


Mantle 不会自动为您保留对象。但是,MTLModel 确实符合,因此可以使用 将模型对象存档到磁盘 NSKeyedArchiver

如果你需要更强大的东西,或者想要避免将整个模型一次保存在内存中,Core Data 可能是更好的选择。

Carthage

github "Mantle/Mantle"


CocoaPods

target 'MyAppOrFramework' do
pod 'Mantle'
end


demo下载及常见问题:https://github.com/Mantle/Mantle

源码:Mantle-master.zip



收起阅读 »

Android 通知栏封装方案

BaseNotification获取此框架 allprojects { repositories { ... maven { url 'https://jitpack.io' } }}dependencies ...
继续阅读 »

BaseNotification

获取此框架 

allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}

dependencies {
implementation 'com.github.Dboy233:BaseNotification:2.0'
}


如何使用此封装框架?

1.自定义你的Notification继承BaseNotification

public abstract class ChatChannelNotify<ChatData> extends BaseNotification<ChatData> {


public ChatChannelNotify(@NotNull ChatData mData) {
super(mData);
//使用add*函数添加对应自定义的布局 然后使用get*
addContentRemoteViews(R.layout.notify_comm_chat_layout);
}
/**
*渠道名字
*/
@NotNull
@Override
public String getChannelName() {
return NotificationConfig.CHANNEL_ID_CHAT;
}
/**
*渠道id
*/
@NotNull
@Override
public String getChannelId() {
return NotificationConfig.CHANNEL_ID_CHAT;
}

@Override
@RequiresApi(api = Build.VERSION_CODES.O)
public void configureChannel(@NotNull NotificationChannel notificationChannel) {
//配置渠道信息。如果没有这个渠道会创建这个渠道,如果有了,这个函数是不会被调用的
notificationChannel.setLockscreenVisibility(NotificationCompat.VISIBILITY_PUBLIC);
notificationChannel.setImportance(NotificationManager.IMPORTANCE_HIGH);
}


@Override
public void configureNotify(@NotNull NotificationCompat.Builder mBuilder) {
//配置你的通知属性
mBuilder.setShowWhen(true)
.setSmallIcon(getData().getSmallIcon())
.setContentTitle(getData().getContentTitle())
.setContentText(getData().getContentText())
.setTicker(getData().getContentTitle())
.setContentInfo(getData().getContentText())
.setAutoCancel(true)
.setSubText(getData().getContentText())
.setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setGroup("chat");
}

/**
*如果你在构造函数使用了add**的函数添加自定义布局 在这里配置你布局展示的数据
*使用**2结尾的函数可链式调用 点击事件做了单独封装
*/
@Override
public void convert(@NotNull BaseRemoteViews mBaseRemoteViews, @NotNull ChatData data) {
ContentRemote contentRemote = mBaseRemoteViews.getContentRemote();
if (contentRemote != null) {
contentRemote
.setImageViewResource2(R.id.notify_chat_head_img, data.getIcon())
.setTextViewText2(R.id.notify_chat_title, data.getContentTitle())
.setTextViewText2(R.id.notify_chat_subtitle, data.getContentText())
.setOnClickPendingIntent2(getNotificationId(), R.id.notify_chat_layout);
}

}
/**
*设置你的通知id
*/
@Override
public int getNotificationId() {
return NotificationConfig.NOTIFICATION_ID;
}

}


2.实例化你的通知并展示

ChatChannelNotify chat=new ChatChannelNotify(new Chat())
chat.show()//显示我们的通知
//更新内容使用 chat.show(new Chat())
//取消通知 chat.cancel()


3.设置点击事件

class ChatActivity : AppCompatActivity(), PendingIntentListener {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//设置点击事件
NotificationControl.addPendingIntentListener(this)
}

/**
*通知点击事件回调
*notifId通知的id
*ViewId点击的viewId
*/
override fun onClick(notifyId: Int, viewId: Int) {
Toast.makeText(this,
"点击事件: notifyId:" + notifyId + " ViewId:" + viewId, Toast.LENGTH_LONG)
.show()
}

override fun onDestroy() {
super.onDestroy()
//销毁点击事件
NotificationControl.removePendingIntentListener(this)
}
}

原文链接:https://github.com/Dboy233/BaseNotification

代码下载:BaseNotification-master.zip



收起阅读 »

Android增量更新

APP自动增量更新抽取的Android自动更新库,目的是几行代码引入更新功能,含服务端代码,欢迎Star,欢迎Fork,谢谢~目录功能介绍流程图效果图与示例apk如何引入更新清单文件简单使用详细说明差分包生成(服务端)依赖License功能介绍 支持...
继续阅读 »

APP自动增量更新

抽取的Android自动更新库,目的是几行代码引入更新功能,含服务端代码,欢迎Star,欢迎Fork,谢谢~

目录

功能介绍

  •  支持全量更新apk,直接升级到最新版本
  •  支持增量更新,只下载补丁包升级
  •  设置仅在wifi环境下更新
  •  支持外部注入网络框架(库默认使用okhttp)
  •  支持前台或后台自动更新
  •  支持基于版本的强制更新
  •  支持对外定制更新提示和更新进度界面
  •  含发布功能后台服务端github (Node.js实现)

流程图

效果图与示例apk

示例1 示例2


如何引入

Gradle引入

step 1

Add the JitPack repository to your build file

allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
dependencies {
implementation 'com.github.itlwy:AppSmartUpdate:v1.0.7'
}


Step 2

Add the dependency

更新清单文件

该清单放置在静态服务器以供App访问,主要用于判断最新的版本,及要更新的版本资源信息等(示例见仓库根目录下的resources目录或直接访问后台代码 github),清单由服务端程序发布apk时生成,详见后台示例:github

{
"minVersion": 100, // app最低支持的版本代码(包含),低于此数值的app将强制更新
"minAllowPatchVersion": 100, // 最低支持的差分版本(包含),低于此数值的app将采取全量更新,否则采用差量
"newVersion": 101, // 当前最新版本代码
"tip": "test update", // 更新提示
"size": 1956631, // 最新apk文件大小
"apkURL": "https://raw.githubusercontent.com/itlwy/AppSmartUpdate/master/resources/app/smart-update.apk", // 最新apk 绝对url地址,也可用相对地址,如下方的"patchURL"字段
"hash": "ea97c8efa490a2eaf7d10b37e63dab0e", // 最新apk文件的md5值
"patchInfo": { // 差分包信息
"v100": { // v100表示-版本代码100的apk需要下载的差分包
"patchURL": "v100/100to101.patch", //差分包地址,相对此UpdateManifest.json文件的地址,也可用绝对地址
"tip": "101 version", // 提示
"hash": "ea97c8efa490a2eaf7d10b37e63dab0e", // 合成后apk(即版本代码101)的文件md5值
"size": 1114810 // 差分包大小
}
}
}


简单使用

1.初始化

public class MyApplication extends Application {

@Override
public void onCreate() {
super.onCreate();
//推荐在Application中初始化
Config config = new Config.Builder()
.isDebug(true)
.build(this);
UpdateManager.getInstance().init(config);
}
}


2.调用

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button mUpdateBtn;
private String manifestJsonUrl = "https://raw.githubusercontent.com/itlwy/AppSmartUpdate/master/resources/UpdateManifest.json";
private IUpdateCallback mCallback;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mUpdateBtn = (Button) findViewById(R.id.update_btn);
mUpdateBtn.setOnClickListener(this);

}

@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.update_btn:
UpdateManager.getInstance().update(this, manifestJsonUrl, null);
break;

}
}
}


详细说明

注册通知回调

  • 其他activity界面需要获知后台更新情况
public void register(IUpdateCallback callback) {...}

public void unRegister(IUpdateCallback callback) {...}

public interface IUpdateCallback {

/**
* 通知无新版本需要更新,运行在主线程
*/
void noNewApp();

/**
* 自动更新准备开始时回调,运行在主线程,可做一些提示等
*/
void beforeUpdate();

/**
* 自动更新的进度回调(分增量和全量更新),运行在主线程
*
* @param percent 当前总进度百分比
* @param totalLength 更新总大小(全量为apk大小,增量为全部补丁大小和)
* @param patchIndex 当前更新的补丁索引(从1开始)
* @param patchCount 需要更新的总补丁数(当为0时表示是增量更新)
*/
void onProgress(int percent, long totalLength, int patchIndex, int patchCount);

/**
* 下载完成,准备更新,运行在主线程
*/
void onCompleted();

/**
* 异常回调,运行在主线程
*
* @param error 异常信息
*/
void onError(String error);

/**
* 用户取消了询问更新对话框
*/
void onCancelUpdate();

/**
* 取消了更新进度对话框,压入后台自动更新,此时由通知栏通知进度
*/
void onBackgroundTrigger();
}


网络框架注入

默认使用okhttp,也可由外部注入,只需实现如下的IHttpManager接口,然后通过new Config.Builder().httpManager(new OkhttpManager())注入即可

public interface IHttpManager {


IResponse syncGet(@NonNull String url, @NonNull Map<String, String> params) throws IOException;

/**
* 异步get
*
* @param url get请求地址
* @param params get参数
* @param callBack 回调
*/
void asyncGet(@NonNull String url, @NonNull Map<String, String> params, @NonNull Callback callBack);


/**
* 异步post
*
* @param url post请求地址
* @param params post请求参数
* @param callBack 回调
*/
void asyncPost(@NonNull String url, @NonNull Map<String, String> params, @NonNull Callback callBack);

/**
* 下载
*
* @param url 下载地址
* @param path 文件保存路径
* @param fileName 文件名称
* @param callback 回调
*/
void download(@NonNull String url, @NonNull String path, @NonNull String fileName, @NonNull FileCallback callback);
}


定制更新交互界面

每个应用的风格都可能是不一样的,因此这里也支持自定义弹出的提示框和进度框,详细见如下代码示例:

  1. 初始化config时需要将内部默认的弹框屏蔽掉

public class MyApplication extends Application {

@Override
public void onCreate() {
super.onCreate();
Config config = new Config.Builder()
.isShowInternalDialog(false)
.build(this);
UpdateManager.getInstance().init(config);
}
}


  1. 自定义对话框,如下(详细代码在MainActivity.java里):

public void registerUpdateCallbak() {
mCallback = new IUpdateCallback() {
@Override
public void noNewApp() {
Toast.makeText(MainActivity.this, "当前已是最新版本!", Toast.LENGTH_LONG).show();
}

@Override
public void hasNewApp(AppUpdateModel appUpdateModel, UpdateManager updateManager, final int updateMethod) {
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
mDialog = builder.setTitle("自动更新提示")
.setMessage(appUpdateModel.getTip())
.setPositiveButton("更新", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
UpdateManager.getInstance().startUpdate(updateMethod);
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {

}
}).create();
mDialog.show();
}

@Override
public void beforeUpdate() {
// 更新开始
mProgressDialog = new ProgressDialog(MainActivity.this);
mProgressDialog.setTitle("更新中...");
mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
mProgressDialog.setMessage("正在玩命更新中...");
mProgressDialog.setMax(100);
mProgressDialog.setProgress(0);
mProgressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
// 退到后台自动更新,进度由通知栏显示
if (UpdateManager.getInstance().isRunning()) {
UpdateManager.getInstance().onBackgroundTrigger();
}
}
});
mProgressDialog.show();
}

@Override
public void onProgress(int percent, long totalLength, int patchIndex, int patchCount) {
String tip;
if (patchCount > 0) {
tip = String.format("正在下载补丁%d/%d", patchIndex, patchCount);
} else {
tip = "正在下载更新中...";
}
mProgressDialog.setProgress(percent);
mProgressDialog.setMessage(tip);
}

@Override
public void onCompleted() {
mProgressDialog.dismiss();
}

@Override
public void onError(String error) {
Toast.makeText(MainActivity.this, error, Toast.LENGTH_LONG).show();
mProgressDialog.dismiss();
}

@Override
public void onCancelUpdate() {

}

@Override
public void onBackgroundTrigger() {
Toast.makeText(MainActivity.this, "转为后台更新,进度由通知栏提示!", Toast.LENGTH_LONG).show();
}
};
UpdateManager.getInstance().register(mCallback);
}


差分包合成(jni)

此部分采用的差分工具为开源bsdiff,用于生成.patch补丁文件,采用jni方式封装一个.so库供java调用,详见"smartupdate"库里的main/cpp目录源码,过程比较简单,就是写个jni的方法来直接调用bsdiff库,目录结构如下:

main
    -cpp
        -bzip2
-CMakeLists.txt
-patchUtils.c
-patchUtils.h
-update-lib.cpp

因为bsdiff还依赖了bzip2,所以这里涉及多个源文件编译链接问题,需要在CMakeLists.txt稍作修改:

# 将当前 "./src/main/cpp" 目录下的所有源文件保存到 "NATIVE_SRC" 中,然后在 add_library 方法调用。
aux_source_directory( . NATIVE_SRC )
# 将 "./src/main/cpp/bzip2" 目录下的子目录bzip2保存到 "BZIP2_BASE" 中,然后在 add_library 方法调用。
aux_source_directory( ./bzip2 BZIP2_BASE )
# 将 BZIP2_BASE 增加到 NATIVE_SRC 中,这样目录的源文件也加入了编译列表中,当然也可以不加到 NATIVE_SRC,直接调用add_library。
list(APPEND NATIVE_SRC ${BZIP2_BASE})

add_library( # Sets the name of the library.
update-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
${NATIVE_SRC})


差分包生成

服务端见github ,使用时将manifestJsonUrl改成部署的服务器地址即可,如下示例代码片段的注释处

public class MainActivity extends AppCompatActivity {
private String manifestJsonUrl = "https://raw.githubusercontent.com/itlwy/AppSmartUpdate/master/resources/UpdateManifest.json";
// private String manifestJsonUrl = "http://192.168.2.107:8000/app/UpdateManifest.json";
...
}


依赖

  • okhttp : com.squareup.okhttp3:okhttp:3.11.0
  • gson : com.google.code.gson:gson:2.8.0
收起阅读 »

OpenCV二维码扫码优化

说明在介绍二维码的优化前,可以参考二维码基础原理了解二维码识别的相关知识。 作者相关博客Android二维码扫描优化Contents 目录概述普通优化解码优化优化相机设置难点灰色角度光照优化尝试1尝试2尝试3项目说明概述随着二维码的流行,几乎所有手持设备都支持...
继续阅读 »

说明

在介绍二维码的优化前,可以参考二维码基础原理了解二维码识别的相关知识。 作者相关博客Android二维码扫描优化

Contents 目录

概述

随着二维码的流行,几乎所有手持设备都支持二维码的扫描识别。对于大部分普通的情景下,使用zxing和zbar就可以很好的完成二维码的识别,但是如果碰到一些特殊的情景,zxing和zbar的识别率并不高。这些特殊的情景包括:

  1. 二维码是灰色的
  2. 大角度斜扫二维码
  3. 由光源引发的摄像头干扰,比如手机扫描屏幕上的二维码会出现条纹或者噪点

普通优化

在介绍难点优化之前,先介绍网上常见的优化点。这些优化点同样被使用在了作者的项目中,具体包括:

解码优化

  1. 减少解码格式
  2. 解码算法优化
  3. 减少解码数据
  4. 解码库Zbar与Zxing融合

优化相机设置

  1. 选择最佳预览尺寸/图片尺寸
  2. 设置适合的相机放大倍数
  3. 调整聚焦时间
  4. 设置自动对焦区域
  5. 调整合理扫描区域

以上普通优化均参考自智能设备上的二维码解码优化, 不过作者在实现过程中,对该链接所对应的项目实现做了一些修改,修改点包括:

  1. 旋转android后置摄像头preview数据时,根据yuv NV21数据的特点进行旋转
  2. 摄像头预览数据中,对应预览框的数据的截取,并非通过PlanarYUVLuminanceSource创建时提供的rect进行截取。而是通过opencv,将yuv数据转换成rgb之后,在进行截取
  3. 摄像头最佳预览尺寸选取时,按照设备屏宽,屏高最接近的方式选取,并没有通过屏幕宽高比进行筛选。因为部分android手机,屏幕的宽高比和摄像头所支持最接近的size的宽高比并不一致(ps:比如nexus 4,屏幕宽高为720/1280, 但是摄像头支持的最接近屏幕宽高比的preview size,都要少几个像素,印象中,好像是 716/1276, 具体多少记不清了)
  4. 支持用户双指对摄像头进行缩放,并未采用链接引用项目的zoom策略
  5. 添加zbar结合zxing一起检测的代码

难点

整个二维码扫描识别过程中,个人认为最关键的一步在于图片的二值化处理。所谓二值化,可以简单理解为给定一个阈值,低于阈值的像素标记为白色,高于阈值的像素标记为黑色。一旦进行二值化后,图片形成黑白两色图,再利用二维码原理进行定位查询并识别解码并不困难。因此能否对一个图片进行正确的二值化极大影响二维码识别率。而以下特殊情况很大程度上会干扰扫描过程中二值化过程:

灰色二维码

如上图,某些二维码是灰色的,或者由于光照的情况下色调比较淡。灰色二维码识别难度在于背景往往比二维码的灰点色度更黑,当用手机对这类灰色二维码进行扫描时,总是或多或少会有一些非二维码的背景显示在扫描框中,而这部分背景由于色调比灰点深,这会造成zxing,或者zbar二值化过程中选择阈值受到干扰,从而无法识别二维码

大角度对二维码进行扫描识别

在这种情况下如果是纯黑色的二维码影响不会太大,但如果是灰色的二维码,就非常容易受到光照的印象。 由于是斜着扫二维码,那么对于摄像头来说,二维码的两边所带来的亮度是不同的。

 

如上图例子,如果是从左侧进行扫描二维码,那么左边由于距离摄像头更近,因此更亮,而右边由于距离摄像头更远,会比较暗,此时整个预览框进行二值化时,非常容易造成右边暗的部分,全部超过阈值而被判断为黑色,进而极大影响二维码的识别

由光源引发的摄像头干扰,比如手机扫描屏幕上的二维码会出现条纹或者噪点

如果二维码位于电脑屏幕中,或者位于其他投影设备里,或者当时处于白炽灯等灯光照耀的情况下,进行扫描,摄像头有时候会受到光源频率的干扰,从而形成条纹状。而某些情况下,当摄像头zoom到一定程度是,形成的早点也会对二维码识别造成极大干扰,如下图所示作者遇到的情况:

 

这类情况,如果二维码是黑色的还好,如果二维码是灰色的,则条纹会对二维码的识别起到很大的干扰,如上边右侧图像所示

优化

针对上述难点,作者尝试通过opencv对相机的preview数据进行预处理,从而提升二维码的扫描识别,作者进行过的尝试如下:

尝试1:通过opencv方法进行预览数据的降噪

作者本想通过opencv的各种降噪滤波算法(例如mediaBlur等),去光照算法(例如RGB归一化等)等对camera预览数据进行处理。可能是作者对于opencv算法理解还不足,也可能是其他原因,测试下来效果都不太好。最好作者选择mediaBlur作为降噪算法进行保留,对于一些椒盐噪点的去除还挺有帮助

尝试2:缩小二值化阈值计算范围

考虑到灰色二维码之所以难以识别是因为背景的色调过暗,拉低了二值化阈值的计算,导致整个二维码的难以识别。因此作者将二值化阈值的计算区域缩小到预览框的2/5,然后再通过计算的阈值对整个预览区域的数据进行二值化。这个效果对于灰色二维码的识别提升很大。可惜的是,只对正面扫描的情况下有用,一旦斜着超过一定角度扫描灰色二维码,往往识别不出。

尝试3:分块对预览区进行二值化

考虑到斜扫二维码不好识别因为矩形预览区域各个部分的亮度不统一,因此作者下一步尝试是,将矩形预览框再次分成4个块,每个块取其靠近中心部分的1/5区域进行阈值计算,最后分块进行二值化后,拼接而成。具体如下示意图所示

之所以分块进行计算阈值,是考虑到不同区域的亮度是不同的,如果整体进行计算的话,会丢失部分有效信息。而之所以选取靠近中心的小部分进行阈值化计算,是因为用户行为通常会将二维码对准预览框的中心,因此中心部分,包含有效亮度信息更为精确,减少背景亮度对整体二值化的影响。这一步尝试之后,大大提升了特殊情景二维码的识别概率。至此,opencv预处理部分到此结束了。

项目说明

项目分成3个module

  • app app module提供demo activity

  • library_qrscan 二维码扫码核心功能,集成zxing,zbar,opencv预处理等功能

  • library_opencv 该module作为隐藏module,setting配置并没有include它,是方便调试opencv功能。该module可以用于生成libProcessing.so,集成在library_qrscan module中。如果用户想使用该module,请按照以下做法:

    1. 打开setting中配置,include该module
    2. 下载opencv for android版本到本地目录,并解压
    3. 修改library_opencv/src/main/jni/Android.mk 中 12行include对应本地opencv的mk路径
    4. 删除library_qrscan/libs/armeabi-v7a/libProcessing.so


收起阅读 »

图片选择器:Matisse

Matisse 是一个为Android精心设计的本地图像和视频选择器。你可以 在活动或片段中使用它 选择包含JPEG、PNG、GIF的图像和包含MPEG、MP4的视频 应用不同的主题,包括两个内置主题和自定义主题 不同的图像加载器 定义自定义筛选规则 ...
继续阅读 »

Matisse 是一个为Android精心设计的本地图像和视频选择器。你可以



  • 在活动或片段中使用它


  • 选择包含JPEG、PNG、GIF的图像和包含MPEG、MP4的视频


  • 应用不同的主题,包括两个内置主题和自定义主题


  • 不同的图像加载器


  • 定义自定义筛选规则















Zhihu Style Dracula Style Preview




下载使用

Gradle:


repositories {
jcenter()
}

dependencies {
implementation 'com.zhihu.android:matisse:$latest_version'
}

混淆

如果用的是 Glide
添加规则:

-dontwarn com.squareup.picasso.**

如果用的是 Picasso 添加规则:


-dontwarn com.bumptech.glide.**

怎样使用他呢?

权限


  • android.permission.READ_EXTERNAL_STORAGE
  • android.permission.WRITE_EXTERNAL_STORAGE

所以,如果您的目标是android6.0+,那么您需要在下一步之前处理运行时权限请求。


Simple usage snippet



启动 MatisseActivity 从 活动的 Activity or Fragment:


Matisse.from(MainActivity.this)
.choose(MimeType.allOf())
.countable(true)
.maxSelectable(9)
.addFilter(new GifSizeFilter(320, 320, 5 * Filter.K * Filter.K))
.gridExpectedSize(getResources().getDimensionPixelSize(R.dimen.grid_expected_size))
.restrictOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)
.thumbnailScale(0.85f)
.imageEngine(new GlideEngine())
.showPreview(false) // Default is `true`
.forResult(REQUEST_CODE_CHOOSE);

主题

有两个内置的主题可以用来开始 MatisseActivity:



  • R.style.Matisse_Zhihu (白天模式)
  • R.style.Matisse_Dracula (夜间模式)

你也可以随心所欲地定义自己的主题。


接收结果

onActivityResult() 回调 Activity or Fragment:


List<Uri> mSelected;

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_CHOOSE && resultCode == RESULT_OK) {
mSelected = Matisse.obtainResult(data);
Log.d("Matisse", "mSelected: " + mSelected);
}
}

github地址:https://github.com/zhihu/matisse
下载地址:matisse-master.zip


收起阅读 »

秀!秀!秀!优秀的富文本库:XRichText

一个Android富文本类库,支持图文混排,支持编辑和预览,支持插入和删除图片。 实现的原理: 使用ScrollView作为最外层布局包含LineaLayout,里面填充TextView和ImageView。删除的时候,根据光标的位置,删除TextView和I...
继续阅读 »

一个Android富文本类库,支持图文混排,支持编辑和预览,支持插入和删除图片。


实现的原理:


  • 使用ScrollView作为最外层布局包含LineaLayout,里面填充TextView和ImageView。
  • 删除的时候,根据光标的位置,删除TextView和ImageView,文本自动合并。
  • 生成的数据为list集合,可自定义处理数据格式。

注意事项


  • V1.4版本开放了图片点击事件接口和删除图片接口,具体使用方式可以参考后面的文档说明,也可以参考Demo实现。
  • V1.6版本升级RxJava到2.2.3版本,RxAndroid到2.1.0版本。设置字体大小时需要带着单位,如app:rt_editor_text_size=”16sp”。
  • V1.9.3及后续版本,xrichtext库中已去掉Glide依赖,开放接口可以自定义图片加载器。具体使用方式可以参考后面的文档说明,也可以参考Demo实现。
  • Demo中图片选择器为知乎开源库Matisse,适配Android 7.0系统使用FileProvider获取图片路径。
  • 开发环境更新为 AS 3.4.2 + Gradle 4.4 + compileSDK 28 + support library 28.0.0,导入项目报版本错误时,请手动修改为自己的版本。
  • 请参考Demo的实现,进行了解本库。可以使用Gradle引入,也可以下载源码进行修改。
  • 如有问题,欢迎提出。欢迎加入QQ群交流:745215148。

截图预览

笔记列表
文字笔记详情
编辑笔记
图片笔记详情

使用方式

1. 作为module导入

把xrichtext作为一个module导入你的工程。


2. gradle依赖

allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}

dependencies {
implementation 'com.github.sendtion:XRichText:1.9.4'
}

如果出现support版本不一致问题,请排除XRichText中的support库,或者升级自己的support库为28.0.0版本。
使用方式:

implementation ('com.github.sendtion:XRichText:1.9.4') {
exclude group: 'com.android.support'
}

具体使用

在xml布局中添加基于EditText编辑器(可编辑)


<com.sendtion.xrichtext.RichTextEditor
android:id="@+id/et_new_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:rt_editor_text_line_space="6dp"
app:rt_editor_image_height="500"
app:rt_editor_image_bottom="10"
app:rt_editor_text_init_hint="在这里输入内容"
app:rt_editor_text_size="16sp"
app:rt_editor_text_color="@color/grey_900"/>

在xml布局中添加基于TextView编辑器(不可编辑)


<com.sendtion.xrichtext.RichTextView
android:id="@+id/tv_note_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:rt_view_text_line_space="6dp"
app:rt_view_image_height="0"
app:rt_view_image_bottom="10"
app:rt_view_text_size="16sp"
app:rt_view_text_color="@color/grey_900"/>

自定义属性

具体参考Demo



  • RichTextView


    rt_view_image_height        图片高度,默认为0自适应,可以设置为固定数值,如500、800等
    rt_view_image_bottom 上下两张图片的间隔,默认10
    rt_view_text_size 文字大小,使用sp单位,如16sp
    rt_view_text_color 文字颜色,使用color资源文件
    rt_view_text_line_space 字体行距,跟TextView使用一样,比如6dp
  • RichTextEditor


    rt_editor_image_height      图片高度,默认为500,可以设置为固定数值,如500、800等,0为自适应高度
    rt_editor_image_bottom 上下两张图片的间隔,默认10
    rt_editor_text_init_hint 默认提示文字,默认为“请输入内容”
    rt_editor_text_size 文字大小,使用sp单位,如16sp
    rt_editor_text_color 文字颜色,使用color资源文件
    rt_editor_text_line_space 字体行距,跟TextView使用一样,比如6dp

生成数据

我把数据保存为了html格式,生成字符串存储到了数据库。


String noteContent = getEditData();

private String getEditData() {
List<RichTextEditor.EditData> editList = et_new_content.buildEditData();
StringBuffer content = new StringBuffer();
for (RichTextEditor.EditData itemData : editList) {
if (itemData.inputStr != null) {
content.append(itemData.inputStr);
} else if (itemData.imagePath != null) {
content.append("<img src=\"").append(itemData.imagePath).append("\"/>");
}
}
return content.toString();
}

显示数据

et_new_content.post(new Runnable() {
@Override
public void run() {
showEditData(content);
}
});

protected void showEditData(String content) {
et_new_content.clearAllLayout();
List<String> textList = StringUtils.cutStringByImgTag(content);
for (int i = 0; i < textList.size(); i++) {
String text = textList.get(i);
if (text.contains("<img")) {
String imagePath = StringUtils.getImgSrc(text);
int width = ScreenUtils.getScreenWidth(this);
int height = ScreenUtils.getScreenHeight(this);
et_new_content.measure(0,0);
Bitmap bitmap = ImageUtils.getSmallBitmap(imagePath, width, height);
if (bitmap != null){
et_new_content.addImageViewAtIndex(et_new_content.getLastIndex(), bitmap, imagePath);
} else {
et_new_content.addEditTextAtIndex(et_new_content.getLastIndex(), text);
}
et_new_content.addEditTextAtIndex(et_new_content.getLastIndex(), text);
}
}
}

图片点击事件

tv_note_content.setOnRtImageClickListener(new RichTextView.OnRtImageClickListener() {
@Override
public void onRtImageClick(String imagePath) {
ArrayList<String> imageList = StringUtils.getTextFromHtml(myContent, true);
int currentPosition = imageList.indexOf(imagePath);
showToast("点击图片:"+currentPosition+":"+imagePath);
// TODO 点击图片预览
}
});

图片加载器使用

请在Application中设置,经测试在首页初始化会出现问题。Demo仅供参考,具体实现根据您使用的图片加载器而变化。


XRichText.getInstance().setImageLoader(new IImageLoader() {
@Override
public void loadImage(String imagePath, ImageView imageView, int imageHeight) {
//如果是网络图片
if (imagePath.startsWith("http://") || imagePath.startsWith("https://")){
Glide.with(getApplicationContext()).asBitmap().load(imagePath).dontAnimate()
.into(new SimpleTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
if (imageHeight > 0) {//固定高度
RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, imageHeight);//固定图片高度,记得设置裁剪剧中
lp.bottomMargin = 10;//图片的底边距
imageView.setLayoutParams(lp);
Glide.with(getApplicationContext()).asBitmap().load(imagePath).centerCrop()
.placeholder(R.mipmap.img_load_fail).error(R.mipmap.img_load_fail).into(imageView);
} else {//自适应高度
Glide.with(getApplicationContext()).asBitmap().load(imagePath)
.placeholder(R.mipmap.img_load_fail).error(R.mipmap.img_load_fail).into(new TransformationScale(imageView));
}
}
});
} else { //如果是本地图片
if (imageHeight > 0) {//固定高度
RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, imageHeight);//固定图片高度,记得设置裁剪剧中
lp.bottomMargin = 10;//图片的底边距
imageView.setLayoutParams(lp);

Glide.with(getApplicationContext()).asBitmap().load(imagePath).centerCrop()
.placeholder(R.mipmap.img_load_fail).error(R.mipmap.img_load_fail).into(imageView);
} else {//自适应高度
Glide.with(getApplicationContext()).asBitmap().load(imagePath)
.placeholder(R.mipmap.img_load_fail).error(R.mipmap.img_load_fail).into(new TransformationScale(imageView));
}
}
}
});

GitHub地址:https://github.com/sendtion/XRichText
下载地址:XRichText-master.zip


收起阅读 »

FBKVOController - 面试聊到KVO如何有效的怒怼面试官!

1.系统KVO的问题2.FBKVOController优点3.FBKVOController的架构设计图4.FBKVOController源码详读5.FBKVOController总结一.系统KVO的问题当观察者被销毁之前,需要手动移除观察者,否则会出现程序异...
继续阅读 »
  • 1.系统KVO的问题
  • 2.FBKVOController优点
  • 3.FBKVOController的架构设计图
  • 4.FBKVOController源码详读
  • 5.FBKVOController总结

一.系统KVO的问题

  • 当观察者被销毁之前,需要手动移除观察者,否则会出现程序异常(向已经销毁的对象发送消息);
  • 可能会对同一个被监听的属性多次添加监听,这样我们会接收到多次监听的回调结果;
  • 当观察者对多个对象的不同属性进行监听,处理监听结果时,需要在监听回调的方法中,作出大量的if判断;
  • 当对同一个被监听的属性进行两次removeObserver时,会导致程序crash。这种情况通常出现在父类中有一个KVO,在父类的dealloc中remove一次,而在子类中再次remove。

二. FBKVOController优点

  • 可以同时对一个对象的多个属性进行监听,写法简洁;
  • 通知不会向已释放的观察者发送消息;
  • 增加了block和自定义操作对NSKeyValueObserving回调的处理支持;
  • 不需要在dealloc 方法中手动移除观察者,而且移除观察者不会抛出异常,当FBKVOController对象被释放时, 观察者被隐式移除;

三.FBKVOController架构设计图






四.FBKVOController源码详解

FBKVOController源码详解分四部分:

  • 私有类_FBKVOInfo,
  • 私有类_FBKVOSharedController
  • FBKVOController,
  • NSObject+FBKVOController的源码解读:
(一)FBKVOController

首先我们创建一个FBKVOController的实例对象时,有以下三种方法,一个类方法和两个对象方法,

//该方法是一个全能初始化的对象方法,其他初始化方法内部均调用该方法
//参数:observer是观察者,retainObserved:表示是否强引用被观察的对象
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved

//该初始化方法内部调用上一个初始化方法,默认强引用被观察的对象
- (instancetype)initWithObserver:(nullable id)observer;

//该初始化方法内部调用上一个初始化方法,默认强引用被观察的对象
+ (instancetype)controllerWithObserver:(nullable id)observer;
NS_DESIGNATED_INITIALIZER;

我们先来看全能初始化方法内部的实现,

  • 该方法对三个实例变量_observer(观察者)
  • _objectInfosMap(NSMapTable,被监听对象->被监听属性集合之间的映射关系)
  • pthread_mutex_init(互斥锁)

//全能初始化方法
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
self = [super init];
if (nil != self) {

//观察者
_observer = observer;

//NSMapTable中的key可以为对象,而且可以对其中的key和value弱引用
NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
_objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];

//对于静态分配的互斥量, 可以把它设置为PTHREAD_MUTEX_INITIALIZER
//对于动态分配的互斥量, 在申请内存(malloc)之后, 通过pthread_mutex_init进行初始化, 并且在释放内存(free)前需要调用pthread_mutex_destroy
pthread_mutex_init(&_lock, NULL);
}
return self;
}

这里请先思考以下问题:

  • 属性observer为何使用weak,它和哪个对象之间会导致循环引用问题,是如何导致循环引用问题的?
  • 为何不使用字典来保存被监听对象和被监听属性集合之间的关系?
  • NSDictionary的局限性有哪些?NSMapTable相对字典,有哪些优点?
  • 互斥锁是为了保证哪些数据的线程安全?

带着这些问题我们来看FBKVOController内部是如何实现监听的,这里我们只看带Block回调的一个监听方法,其他几个方法和这个方法内部实现是相同的。下面的方法内部做了如下工作:

  • 1.传入的参数keyPath,block为空时,程序闪退,同时报出误提示;
  • 2.对传入参数为空的判读;
  • 3.利用传入的参数创建_FBKVOInfo对象;
  • 4.调用内部私有方法实现注册监听;
//观察者监听object中健值路径(keyPath)所对应属性的变化
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
//NSAssert是一个预处理宏, 它可以让开发者比较便捷的捕获错误, 让程序闪退, 同时报出错误提示
NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);

//首先判断被监听的对象是否为空,被监听的健值路径是否为空,回调的block是否为空
if (nil == object || 0 == keyPath.length || NULL == block) {
return;
}

// 根据传进来的参数创建_FBKVOInfo对象,将这些参数封装到_FBKVOInfo对象中
_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];

// 监听对象object的属性信息(_FBKVOInfo对象)
[self _observe:object info:info];
}

该私有方法内部并没有实现真正的注册监听,这里使用NSMapTable保存了被监听对象object-> _FBKVOInfo对象集合的关系,具体的监听是在_FBKVOSharedController类中实现的。观察者可以监听多个对象,而每个对象中可能有多个属性被监听


内部实现思路:

  • 对当前线程访问的数据_objectInfosMap进行加锁;
  • 根据被监听对象object到_objectInfosMap取出被监听的属性信息对象集合infos;
  • 判断被监听的属性对象info是否存在集合中;
  • 如果已经存在,则不需要再次添加监听,防止多次监听;
  • 如果获取的集合infos为空,则建存放_FBKVOInfo对象的集合infos,保存映射关系:object->infos;
  • 将被监听的信息_FBKVOInfo对象存到集合infos中;
  • 解锁,其他线程可以访问该数据;
  • 调用_FBKVOSharedController 的方法实现监听;
//该方法是内部私有方法
- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
//先加锁,访问_objectInfosMap
pthread_mutex_lock(&_lock);

//到_objectInfosMap中根据key(被监听的对象)获取被监听的属性信息集合
NSMutableSet *infos = [_objectInfosMap objectForKey:object];

//判断infos集合中是否存在被监听属性信息对象info
_FBKVOInfo *existingInfo = [infos member:info];

//被监听对象的属性已经存在,不需要再次监听,防止多次添加监听
if (nil != existingInfo) {

//解锁,其他线程可以再次访问_objectInfosMap中的数据
pthread_mutex_unlock(&_lock);
return;
}

//根据被监听对象在_objectInfosMap获取的被监听属性信息的集合为空
if (nil == infos) {
//懒加载创建存放_FBKVOInfo对象的set集合infos
infos = [NSMutableSet set];

//保存被监听对象和被监听属性信息的映射关系object->infos
[_objectInfosMap setObject:infos forKey:object];
}

// 将被监听的信息_FBKVOInfo对象存到集合infos中
[infos addObject:info];

//解锁
pthread_mutex_unlock(&_lock);

//最终的监听方法是通过_FBKVOSharedController中的方法来实现
//_FBKVOSharedController内部实现系统KVO方法
[[_FBKVOSharedController sharedController] observe:object info:info];
}
(二)_FBKVOInfo

_FBKVOInfo私有类的内部很简单,没有任何业务逻辑,只是一个简单的Model,主要是将以下的实例变量封装到对象中,方便访问:

{
@public
//weak,防止循环引用
__weak FBKVOController *_controller;
//被监听属性的健值路径
NSString *_keyPath;

//NSKeyValueObservingOptionNew:观察修改前的值
// NSKeyValueObservingOptionOld:观察修改后的值
//NSKeyValueObservingOptionInitial:观察最初的值(在注册观察服务时会调用一次触发方法)
//NSKeyValueObservingOptionPrior:分别在值修改前后触发方法(一次修改有两次触发)
NSKeyValueObservingOptions _options;

//被监听属性值变化时的回调方法
SEL _action;

//上下文信息(void * 任何类型)
void *_context;
//被监听属性值变化时的回调block
FBKVONotificationBlock _block;
//监听状态
_FBKVOInfoState _state;
}

_FBKVOInfo私有类提供了一个全能初始化方法,来初始化以上实例变量。其他几个部分初始化方法内部均调用该全能初始化方法。

//全能初始化方法
- (instancetype)initWithController:(FBKVOController *)controller
keyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
block:(nullable FBKVONotificationBlock)block
action:(nullable SEL)action
context:(nullable void *)context
{
self = [super init];
if (nil != self) {
_controller = controller;
_block = [block copy];
_keyPath = [keyPath copy];
_options = options;
_action = action;
_context = context;
}
return self;
}


优化判断对象相等性的效率:
  • 1.首先判断hash值是否相等,若相等则进行第2步;若不等,则直接判断不等;hash值是对象判等的必要非充分条件;(即没它一定不行,有它不一定行)
  • 2.在hash值相等的情况下,再进行对象判等, 作为判等的结果
//当重写hash方法时,我们可以将关键属性的hash值进行位或运算来作为hash值
- (NSUInteger)hash
{
return [_keyPath hash];
}

/**
对于基本类型, ==运算符比较的是值;
对于对象类型, ==运算符比较的是对象的地址(即是否为同一对象)
*/

- (BOOL)isEqual:(id)object
{
//判断对象是否为空,若为空,则不相等
if (nil == object) {
return NO;
}

//判断对象的地址是否相等,若相等,则为同一个对象(即是否为同一个对象)
if (self == object) {
return YES;
}

//判断是否是同一类型,这样可以提高判等的效率, 还可以避免隐式类型转换带来的潜在风险
if (![object isKindOfClass:[self class]]) {
return NO;
}

//对各个属性分别使用默认判等方法进行判断
//返回所有属性判等的与结果
return [_keyPath isEqualToString:((_FBKVOInfo *)object)->_keyPath];
}

//输出对象的调试信息

//description: 使用NSLog从控制台输出对象的信息
//debugDescription:通过断点po打印输出对象的信息
- (NSString *)debugDescription
{
NSMutableString *s = [NSMutableString stringWithFormat:@"<%@:%p keyPath:%@", NSStringFromClass([self class]), self, _keyPath];
if (0 != _options) {
[s appendFormat:@" options:%@", describe_options(_options)];
}
if (NULL != _action) {
[s appendFormat:@" action:%@", NSStringFromSelector(_action)];
}
if (NULL != _context) {
[s appendFormat:@" context:%p", _context];
}
if (NULL != _block) {
[s appendFormat:@" block:%p", _block];
}
[s appendString:@">"];
return s;
}

  • 请分析如果将实例变量__weak FBKVOController *_controller前的 __weak去掉,它和_FBKVOInfo对象之间的循环引用环是如何形成的?
(三)_FBKVOSharedController

_FBKVOSharedController私有类内部实现了系统KVO的方法,用来接收和转发KVO的通知。接口中提供了监听和移除监听的方法。其接口如下:


@interface _FBKVOSharedController : NSObject

// 单例初始化方法
+ (instancetype)sharedController;

// 监听object的属性
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info;

//移除对object中属性的监听
- (void)unobserve:(id)object info:(nullable _FBKVOInfo *)info;

// 移除对object中多个属性的监听
- (void)unobserve:(id)object infos:(nullable NSSet *)infos;

@end
_FBKVOSharedController私有类内部有两个私有成员变量,_infos是用来存放_FBKVOInfo对象,_infos可以对其中的成员变量弱引用,这也是为何使用NSHashTable,而不使用NSSet来存放_FBKVOInfo对象的原因。_mutex是互斥锁:

{
//存放被监听属性的信息对象
NSHashTable<_FBKVOInfo *> *_infos;
//互斥锁
pthread_mutex_t _mutex;
}

_FBKVOSharedController私有类的初始化方法,支持iOS 系统和Mac系统,初始化实例变量_infos,指定了_infos对存放在其中的成员变量弱引用,及判等性方式:

//提供全局的单例初始化方法,该单例对象的生命周期与程序的生命周期相同
+ (instancetype)sharedController
{
static _FBKVOSharedController *_controller = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_controller = [[_FBKVOSharedController alloc] init];
});
return _controller;
}
//初始化成员变量_infos和_mutex
- (instancetype)init
{
self = [super init];
if (nil != self) {
//初始化实例变量
NSHashTable *infos = [NSHashTable alloc];

// iOS 系统下:hashTable中的对象是弱引用,对象的判等方式:位移指针的hash值和直接判等
#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
_infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];

//MAC系统下
#elif defined(__MAC_OS_X_VERSION_MIN_REQUIRED)
if ([NSHashTable respondsToSelector:@selector(weakObjectsHashTable)]) {
_infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
} else {
// silence deprecated warnings
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
_infos = [infos initWithOptions:NSPointerFunctionsZeroingWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
#pragma clang diagnostic pop
}

#endif
//初始化互斥锁
pthread_mutex_init(&_mutex, NULL);
}
return self;
}

- (void)dealloc
{
//对象被销毁时,销毁互斥锁
pthread_mutex_destroy(&_mutex);
}
_FBKVOSharedController在这个方法中,调用系统KVO方法,将自己注册为观察者,思路如下:
1.首先将被监听的信息对象_FBKVOInfo保存到_infos中;
2.然后调用系统KVO方法将自己注册为被监听对象object的观察者;
3.最后修改监听的状态;当不再监听时,安全移除观察者;

//添加监听
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
//被监听的属性信息_FBKVOInfo对象为空时,直接返回
if (nil == info) {
return;
}

// 加锁,防止多线程访问时,出现数据竞争
pthread_mutex_lock(&_mutex);

// 将被监听的属性信息info对象添加到_infos中,_infos对成员变量info是弱引用
[_infos addObject:info];

//添加完成之后,解锁,其他线程可以访问
pthread_mutex_unlock(&_mutex);

// 添加监听
[object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];

//修改监听状态
if (info->_state == _FBKVOInfoStateInitial) {

info->_state = _FBKVOInfoStateObserving;

} else if (info->_state == _FBKVOInfoStateNotObserving) {

//不再监听时安全移除观察者
// this could happen when `NSKeyValueObservingOptionInitial` is one of the NSKeyValueObservingOptions,
// and the observer is unregistered within the callback block.
// at this time the object has been registered as an observer (in Foundation KVO),
// so we can safely unobserve it.
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
}
实现系统KVO监听回调的方法

//被监听属性更改时的回调
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary<NSString *, id> *)change
context:(nullable void *)context
{
NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change);

_FBKVOInfo *info;
{
pthread_mutex_lock(&_mutex);
//确定_infos是否包含给定的对象context,若存在返回该对象,否则返回nil;
//所使用的相等性比较取决于所选择的选项
//例如,使用NSPointerFunctionsObjectPersonality选项将使用isEqual:方法来判断相等。
info = [_infos member:(__bridge id)context];
pthread_mutex_unlock(&_mutex);
}

//通过上下文参数context传过来的被监听的_FBKVOInfo对象,已经存在_infos中
if (nil != info) {

//_FBKVOSharedController对象强引用FBKVOController对象,防止被提前释放
//因为在_FBKVOInfo中,对FBKVOController对象是弱引用
FBKVOController *controller = info->_controller;
if (nil != controller) {

//强引用观察者,在FBKVOController中,FBKVOController对象弱引用观察者observer,防止在使用时已经被释放
id observer = controller.observer;
if (nil != observer) {

//使用自定义block回传监听结果
if (info->_block) {

NSDictionary<NSString *, id> *changeWithKeyPath = change;

//将keyPath添加到字典中以便在观察多个keyPath时,能够清晰知道监听的是哪个keyPath
if (keyPath) {
NSMutableDictionary<NSString *, id> *mChange = [NSMutableDictionary dictionaryWithObject:keyPath forKey:FBKVONotificationKeyPathKey];
[mChange addEntriesFromDictionary:change];
changeWithKeyPath = [mChange copy];
}
info->_block(observer, object, changeWithKeyPath);

} else if (info->_action) {
//使用自定义方法回传监听结果
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[observer performSelector:info->_action withObject:change withObject:object];
#pragma clang diagnostic pop
} else {
//使用系统默认方法回传监听结果
[observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
}
}
}
}
}

_FBKVOSharedController实现了移除观察者的方法,思路如下:

  • 1.首先从_infos中移除被监听的属性信息对象info;
  • 2.然后根据监听状态,通过调用系统的方法,移除正在被监听的属性信息对象info;
  • 3.最后修改监听状态;

- (void)unobserve:(id)object info:(nullable _FBKVOInfo *)info
{
if (nil == info) {
return;
}

//先从HashTable中移除被监听的属性信息对象
pthread_mutex_lock(&_mutex);
[_infos removeObject:info];
pthread_mutex_unlock(&_mutex);

// 当正在监听时,则移除监听
if (info->_state == _FBKVOInfoStateObserving) {
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
//修改被监听的状态
info->_state = _FBKVOInfoStateNotObserving;
}

(四)NSObject+FBKVOController

NSObject+FBKVOController 分类比较简单,它主要通过runtime方法,以懒加载的形式给 NSObject ,创建并关联一个 FBKVOController 的对象。



@interface NSObject (FBKVOController)
@property (nonatomic, strong) FBKVOController *KVOController;
@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;
@end

五.FBKVOController总结

FBKVOController是线程安全的,相对于系统的KVO而言,使用起来更方便,安全,简洁。

  • 1.NSHashTable和NSMapTable的使用;
  • 2.互斥锁pthread_mutex_t的使用
  • 3.FBKVOController和Observer之间循环引用的形成和解决;
  • 4.FBKVOController和_FBKVOInfo之间循环引用的形成和解决;


作者:Cooci
链接:https://www.jianshu.com/p/65e345a7aa21

收起阅读 »

没对象么?那就来了解Java创建对象详解

对象是对类的实例化。对象具有状态和行为,变量用来表明对象的状态,方法表明对象所具有的行为。Java 对象的生命周期包括创建、使用和清除,本文详细介绍对象的创建 Java虚拟机内存架构模型详解 1.使用new创建对象 使用new关键字创建对象应该是最常见的一种...
继续阅读 »

对象是对类的实例化。对象具有状态和行为,变量用来表明对象的状态,方法表明对象所具有的行为。Java 对象的生命周期包括创建、使用和清除,本文详细介绍对象的创建



Java虚拟机内存架构模型详解


1.使用new创建对象


使用new关键字创建对象应该是最常见的一种方式,但我们应该知道,使用new创建对象会增加耦合度。无论使用什么框架,都要减少new的使用以降低耦合度。


public class Hello
{
public void sayWorld()
{
System.out.println("Hello world!");
}

}
public class NewClass
{
public static void main(String[] args)
{
Hello h = new Hello();
h.sayWorld();
}
}
复制代码

2.使用反射的机制创建对象


使用Class类的newInstance方法


  Hello类的代码不变,NewClass类的代码如下:


public class NewClass
{
public static void main(String[] args)
{
try
{
Class heroClass = Class.forName("yunche.test.Hello");
Hello h =(Hello) heroClass.newInstance();
h.sayWorld();
}
catch (ClassNotFoundException e)
{
e.printStackTrace();
}
catch (IllegalAccessException e)
{
e.printStackTrace();
}
catch (InstantiationException e)
{
e.printStackTrace();
}

}
}
复制代码

使用Constructor类的newInstance方法


public class NewClass
{
public static void main(String[] args)
{
try
{
//获取类对象
Class heroClass = Class.forName("yunche.test.Hello");

//获取构造器
Constructor constructor = heroClass.getConstructor();
Hello h =(Hello) constructor.newInstance();
h.sayWorld();
}
catch (NoSuchMethodException e)
{
e.printStackTrace();
}
catch (InvocationTargetException e)
{
e.printStackTrace();
}
catch (IllegalAccessException e)
{
e.printStackTrace();
}
catch (InstantiationException e)
{
e.printStackTrace();
}
catch (ClassNotFoundException e)
{
e.printStackTrace();
}

}
}
复制代码

3.采用clone


  clone时,需要已经有一个分配了内存的源对象,创建新对象时,首先应该分配一个和源对象一样大的内存空间。


  要调用clone方法需要实现Cloneable接口,由于clone方法是protected的,所以修改Hello类。


public class Hello implements Cloneable
{
public void sayWorld()
{
System.out.println("Hello world!");

}

public static void main(String[] args)
{
Hello h1 = new Hello();
try
{
Hello h2 = (Hello)h1.clone();
h2.sayWorld();
}
catch (CloneNotSupportedException e)
{
e.printStackTrace();
}
}
}
复制代码

4.采用序列化机制


  使用序列化时,要实现实现Serializable接口,将一个对象序列化到磁盘上,而采用反序列化可以将磁盘上的对象信息转化到内存中。


public class Serialize
{
public static void main(String[] args)
{
Hello h = new Hello();

//准备一个文件用于存储该对象的信息
File f = new File("hello.obj");

try(FileOutputStream fos = new FileOutputStream(f);
ObjectOutputStream oos = new ObjectOutputStream(fos);
FileInputStream fis = new FileInputStream(f);
ObjectInputStream ois = new ObjectInputStream(fis)
)
{
//序列化对象,写入到磁盘中
oos.writeObject(h);
//反序列化对象
Hello newHello = (Hello)ois.readObject();

//测试方法
newHello.sayWorld();
}
catch (FileNotFoundException e)
{
e.printStackTrace();
}
catch (IOException e)
{
e.printStackTrace();
}
catch (ClassNotFoundException e)
{
e.printStackTrace();
}
}
}

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

iOS必备装X技能-NSOperationQueue 控制串行执行、并发执行

 NSOperationQueue 控制串行执行、并发执行NSOperationQueue 创建的自定义队列同时具有串行、并发功能,那么他的串行功能是如何实现的?这里有个关键属性 maxConcurrentOperationCount,叫做...
继续阅读 »

 NSOperationQueue 控制串行执行、并发执行

NSOperationQueue 创建的自定义队列同时具有串行、并发功能,那么他的串行功能是如何实现的?


这里有个关键属性 maxConcurrentOperationCount,叫做最大并发操作数。用来控制一个特定队列中可以有多少个操作同时参与并发执行。

注意:这里 maxConcurrentOperationCount 控制的不是并发线程的数量,而是一个队列中同时能并发执行的最大操作数。而且一个操作也并非只能在一个线程中运行。


最大并发操作数:maxConcurrentOperationCount
  • maxConcurrentOperationCount 默认情况下为-1,表示不进行限制,可进行并发执行。
  • maxConcurrentOperationCount 为1时,队列为串行队列。只能串行执行。
  • maxConcurrentOperationCount 大于1时,队列为并发队列。操作并发执行,当然这个值不应超过系统限制,即使自己设置一个很大的值,系统也会自动调整为 min{自己设定的值,系统设定的默认最大值}。

/**
* 设置 MaxConcurrentOperationCount(最大并发操作数)
*/

- (void)setMaxConcurrentOperationCount {

// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];

// 2.设置最大并发操作数
queue.maxConcurrentOperationCount = 1; // 串行队列
// queue.maxConcurrentOperationCount = 2; // 并发队列
// queue.maxConcurrentOperationCount = 8; // 并发队列

// 3.添加操作
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"4---%@", [NSThread currentThread]); // 打印当前线程
}
}];
}

当最大并发操作数为1时,操作是按顺序串行执行的,并且一个操作完成之后,下一个操作才开始执行。当最大操作并发数为2时,操作是并发执行的,可以同时执行两个操作。而开启线程数量是由系统决定的,不需要我们来管理

这样看来,是不是比 GCD 还要简单了许多

 NSOperation 操作依赖

NSOperation、NSOperationQueue 最吸引人的地方是它能添加操作之间的依赖关系。通过操作依赖,我们可以很方便的控制操作之间的执行先后顺序。NSOperation 提供了3个接口供我们管理和查看依赖。

  • - (void)addDependency:(NSOperation *)op; 添加依赖,使当前操作依赖于操作 op 的完成。
  • - (void)removeDependency:(NSOperation *)op; 移除依赖,取消当前操作对操作 op 的依赖。
  • @property (readonly, copy) NSArray<NSOperation *> *dependencies; 在当前操作开始执行之前完成执行的所有操作对象数组。

当然,我们经常用到的还是添加依赖操作。现在考虑这样的需求,比如说有 A、B 两个操作,其中 A 执行完操作,B 才能执行操作。

如果使用依赖来处理的话,那么就需要让操作 B 依赖于操作 A。具体代码如下:


/**
* 操作依赖
* 使用方法:addDependency:
*/

- (void)addDependency {

// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];

// 2.创建操作
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];

// 3.添加依赖
[op2 addDependency:op1]; // 让op2 依赖于 op1,则先执行op1,在执行op2

// 4.添加操作到队列中
[queue addOperation:op1];
[queue addOperation:op2];
}

通过添加操作依赖,无论运行几次,其结果都是 op1 先执行,op2 后执行

NSOperation 优先级

NSOperation 提供了queuePriority(优先级)属性,queuePriority属性适用于同一操作队列中的操作,不适用于不同操作队列中的操作。默认情况下,所有新创建的操作对象优先级都是NSOperationQueuePriorityNormal。但是我们可以通过setQueuePriority:方法来改变当前操作在同一队列中的执行优先级。


// 优先级的取值
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
};

对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)。

那么,什么样的操作才是进入就绪状态的操作呢?

  • 当一个操作的所有依赖都已经完成时,操作对象通常会进入准备就绪状态,等待执行。

举个例子,现在有4个优先级都是 NSOperationQueuePriorityNormal(默认级别)的操作:op1,op2,op3,op4。其中 op3 依赖于 op2,op2 依赖于 op1,即 op3 -> op2 -> op1。现在将这4个操作添加到队列中并发执行。

  • 因为 op1 和 op4 都没有需要依赖的操作,所以在 op1,op4 执行之前,就是处于准备就绪状态的操作。
  • 而 op3 和 op2 都有依赖的操作(op3 依赖于 op2,op2 依赖于 op1),所以 op3 和 op2 都不是准备就绪状态下的操作。

理解了进入就绪状态的操作,那么我们就理解了queuePriority 属性的作用对象。

  • queuePriority 属性决定了进入准备就绪状态下的操作之间的开始执行顺序。并且,优先级不能取代依赖关系。
  • 如果一个队列中既包含高优先级操作,又包含低优先级操作,并且两个操作都已经准备就绪,那么队列先执行高优先级操作。比如上例中,如果 op1 和 op4 是不同优先级的操作,那么就会先执行优先级高的操作。
  • 如果,一个队列中既包含了准备就绪状态的操作,又包含了未准备就绪的操作,未准备就绪的操作优先级比准备就绪的操作优先级高。那么,虽然准备就绪的操作优先级低,也会优先执行。优先级不能取代依赖关系。如果要控制操作间的启动顺序,则必须使用依赖关系。


 NSOperation、NSOperationQueue 线程间的通信


在 iOS 开发过程中,我们一般在主线程里边进行 UI 刷新,例如:点击、滚动、拖拽等事件。我们通常把一些耗时的操作放在其他线程,比如说图片下载、文件上传等耗时操作。而当我们有时候在其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程之间的通讯

/**
* 线程间通信
*/

- (void)communication {

// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];

// 2.添加操作
[queue addOperationWithBlock:^{
// 异步进行耗时操作
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}

// 回到主线程
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// 进行一些 UI 刷新等操作
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];
}];
}
通过线程间的通信,先在其他线程中执行操作,等操作执行完了之后再回到主线程执行主线程的相应操作

NSOperation、NSOperationQueue 线程同步和线程安全

  • 线程安全:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
    若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作(更改变量),一般都需要考虑线程同步,否则的话就可能影响线程安全。
  • 线程同步:可理解为线程 A 和 线程 B 一块配合,A 执行到一定程度时要依靠线程 B 的某个结果,于是停下来,示意 B 运行;B 依言执行,再将结果给 A;A 再继续操作。

举个简单例子就是:两个人在一起聊天。两个人不能同时说话,避免听不清(操作冲突)。等一个人说完(一个线程结束操作),另一个再说(另一个线程再开始操作)。

下面,我们模拟火车票售卖的方式,实现 NSOperation 线程安全和解决线程同步问题。
场景:总共有50张火车票,有两个售卖火车票的窗口,一个是北京火车票售卖窗口,另一个是上海火车票售卖窗口。两个窗口同时售卖火车票,卖完为止。


NSOperation、NSOperationQueue 非线程安全

先来看看不考虑线程安全的代码:

/**
* 非线程安全:不使用 NSLock
* 初始化火车票数量、卖票窗口(非线程安全)、并开始卖票
*/

- (void)initTicketStatusNotSave {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程

self.ticketSurplusCount = 50;

// 1.创建 queue1,queue1 代表北京火车票售卖窗口
NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];
queue1.maxConcurrentOperationCount = 1;

// 2.创建 queue2,queue2 代表上海火车票售卖窗口
NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
queue2.maxConcurrentOperationCount = 1;

// 3.创建卖票操作 op1
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
[self saleTicketNotSafe];
}];

// 4.创建卖票操作 op2
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
[self saleTicketNotSafe];
}];

// 5.添加操作,开始卖票
[queue1 addOperation:op1];
[queue2 addOperation:op2];
}

/**
* 售卖火车票(非线程安全)
*/

- (void)saleTicketNotSafe {
while (1) {

if (self.ticketSurplusCount > 0) {
//如果还有票,继续售卖
self.ticketSurplusCount--;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
} else {
NSLog(@"所有火车票均已售完");
break;
}
}
}

在不考虑线程安全,不使用 NSLock 情况下,得到票数是错乱的,这样显然不符合我们的需求,所以我们需要考虑线程安全问题

NSOperation、NSOperationQueue 线程安全

线程安全解决方案:可以给线程加锁,在一个线程执行该操作的时候,不允许其他线程进行操作。iOS 实现线程加锁有很多种方式。@synchronized、 NSLock、NSRecursiveLock、NSCondition、NSConditionLock、pthread_mutex、dispatch_semaphore、OSSpinLock、atomic(property) set/ge等等各种方式。这里我们使用 NSLock 对象来解决线程同步问题。NSLock 对象可以通过进入锁时调用 lock 方法,解锁时调用 unlock 方法来保证线程安全。

考虑线程安全的代码:


/**
* 线程安全:使用 NSLock 加锁
* 初始化火车票数量、卖票窗口(线程安全)、并开始卖票
*/


- (void)initTicketStatusSave {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程

self.ticketSurplusCount = 50;

self.lock = [[NSLock alloc] init]; // 初始化 NSLock 对象

// 1.创建 queue1,queue1 代表北京火车票售卖窗口
NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];
queue1.maxConcurrentOperationCount = 1;

// 2.创建 queue2,queue2 代表上海火车票售卖窗口
NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
queue2.maxConcurrentOperationCount = 1;

// 3.创建卖票操作 op1
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
[self saleTicketSafe];
}];

// 4.创建卖票操作 op2
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
[self saleTicketSafe];
}];

// 5.添加操作,开始卖票
[queue1 addOperation:op1];
[queue2 addOperation:op2];
}

/**
* 售卖火车票(线程安全)
*/

- (void)saleTicketSafe {
while (1) {

// 加锁
[self.lock lock];

if (self.ticketSurplusCount > 0) {
//如果还有票,继续售卖
self.ticketSurplusCount--;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
}

// 解锁
[self.lock unlock];

if (self.ticketSurplusCount <= 0) {
NSLog(@"所有火车票均已售完");
break;
}
}
}

在考虑了线程安全,使用 NSLock 加锁、解锁机制的情况下,得到的票数是正确的,没有出现混乱的情况。我们也就解决了多个线程同步的问题

NSOperation 常用属性和方法

  1. 取消操作方法
    • - (void)cancel; 可取消操作,实质是标记 isCancelled 状态。
  2. 判断操作状态方法
    • - (BOOL)isFinished; 判断操作是否已经结束。
    • - (BOOL)isCancelled; 判断操作是否已经标记为取消。
    • - (BOOL)isExecuting; 判断操作是否正在在运行。
    • - (BOOL)isReady; 判断操作是否处于准备就绪状态,这个值和操作的依赖关系相关。
  3. 操作同步
    • - (void)waitUntilFinished; 阻塞当前线程,直到该操作结束。可用于线程执行顺序的同步。
    • - (void)setCompletionBlock:(void (^)(void))block; completionBlock 会在当前操作执行完毕时执行 completionBlock。
    • - (void)addDependency:(NSOperation *)op; 添加依赖,使当前操作依赖于操作 op 的完成。
    • - (void)removeDependency:(NSOperation *)op; 移除依赖,取消当前操作对操作 op 的依赖。
    • @property (readonly, copy) NSArray<NSOperation *> *dependencies; 在当前操作开始执行之前完成执行的所有操作对象数组。


 NSOperationQueue 常用属性和方法

  • 取消/暂停/恢复操作
    • - (void)cancelAllOperations; 可以取消队列的所有操作。
    • - (BOOL)isSuspended; 判断队列是否处于暂停状态。 YES 为暂停状态,NO 为恢复状态。
    • - (void)setSuspended:(BOOL)b; 可设置操作的暂停和恢复,YES 代表暂停队列,NO 代表恢复队列。
  • 操作同步
    • - (void)waitUntilAllOperationsAreFinished; 阻塞当前线程,直到队列中的操作全部执行完毕。
  • 添加/获取操作`
    • - (void)addOperationWithBlock:(void (^)(void))block; 向队列中添加一个 NSBlockOperation 类型操作对象。
    • - (void)addOperations:(NSArray *)ops waitUntilFinished:(BOOL)wait; 向队列中添加操作数组,wait 标志是否阻塞当前线程直到所有操作结束
    • - (NSArray *)operations; 当前在队列中的操作数组(某个操作执行结束后会自动从这个数组清除)。
    • - (NSUInteger)operationCount; 当前队列中的操作数。
  • 获取队列
    • + (id)currentQueue; 获取当前队列,如果当前线程不是在 NSOperationQueue 上运行则返回 nil。
    • + (id)mainQueue; 获取主队列。


  • 注意:

    1. 这里的暂停和取消(包括操作的取消和队列的取消)并不代表可以将当前的操作立即取消,而是当当前的操作执行完毕之后不再执行新的操作。
    2. 暂停和取消的区别就在于:暂停操作之后还可以恢复操作,继续向下执行;而取消操作之后,所有的操作就清空了,无法再接着执行剩下的操作。


    作者:Cooci
    链接:https://www.jianshu.com/p/5ee0aa045127





    收起阅读 »

    iOS面试-与面试官盘NSOperation、NSOperationQueue

    NSOperation、NSOperationQueue 是苹果提供给我们的一套多线程解决方案。实际上 NSOperation、NSOperationQueue 是基于 GCD 更高一层的封装,完全面向对象。但是比 GCD 更简单易用、代码可读性也更高。为什么...
    继续阅读 »

    NSOperation、NSOperationQueue 是苹果提供给我们的一套多线程解决方案。实际上 NSOperation、NSOperationQueue 是基于 GCD 更高一层的封装,完全面向对象。但是比 GCD 更简单易用、代码可读性也更高。

    为什么要使用 NSOperation、NSOperationQueue?

    1. 可添加完成的代码块,在操作完成后执行。
    2. 添加操作之间的依赖关系,方便的控制执行顺序。
    3. 设定操作执行的优先级。
    4. 可以很方便的取消一个操作的执行。
    5. 使用 KVO 观察对操作执行状态的更改:isExecuteing、isFinished、isCancelled。

    2. NSOperation、NSOperationQueue 操作和操作队列

    既然是基于 GCD 的更高一层的封装。那么,GCD 中的一些概念同样适用于 NSOperation、NSOperationQueue。在 NSOperation、NSOperationQueue 中也有类似的任务(操作)队列(操作队列)的概念。

    • 操作(Operation):
      • 执行操作的意思,换句话说就是你在线程中执行的那段代码。
      • 在 GCD 中是放在 block 中的。在 NSOperation 中,我们使用 NSOperation 子类 NSInvocationOperationNSBlockOperation,或者自定义子类来封装操作。
    • 操作队列(Operation Queues):
      • 这里的队列指操作队列,即用来存放操作的队列。不同于 GCD 中的调度队列 FIFO(先进先出)的原则。NSOperationQueue 对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)。
      • 操作队列通过设置最大并发操作数(maxConcurrentOperationCount)来控制并发、串行。
      • NSOperationQueue 为我们提供了两种不同类型的队列:主队列和自定义队列。主队列运行在主线程之上,而自定义队列在后台执行。

    3. NSOperation、NSOperationQueue 使用步骤

    NSOperation 需要配合 NSOperationQueue 来实现多线程。因为默认情况下,NSOperation 单独使用时系统同步执行操作,配合 NSOperationQueue 我们能更好的实现异步执行。

    NSOperation 实现多线程的使用步骤分为三步:

    1. 创建操作:先将需要执行的操作封装到一个 NSOperation 对象中。
    2. 创建队列:创建 NSOperationQueue 对象。
    3. 将操作加入到队列中:将 NSOperation 对象添加到 NSOperationQueue 对象中。

    之后呢,系统就会自动将 NSOperationQueue 中的 NSOperation 取出来,在新线程中执行操作。

    下面我们来学习下 NSOperation 和 NSOperationQueue 的基本使用。

    4. NSOperation 和 NSOperationQueue 基本使用

    4.1 创建操作

    NSOperation 是个抽象类,不能用来封装操作。我们只有使用它的子类来封装操作。我们有三种方式来封装操作。

    1. 使用子类 NSInvocationOperation
    2. 使用子类 NSBlockOperation
    3. 自定义继承自 NSOperation 的子类,通过实现内部相应的方法来封装操作。

    在不使用 NSOperationQueue,单独使用 NSOperation 的情况下系统同步执行操作,下面我们学习以下操作的三种创建方式。


    4.1.1 使用子类 NSInvocationOperation

    /**
    * 使用子类 NSInvocationOperation
    */

    - (void)useInvocationOperation {

    // 1.创建 NSInvocationOperation 对象
    NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];

    // 2.调用 start 方法开始执行操作
    [op start];
    }

    /**
    * 任务1
    */

    - (void)task1 {
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
    }
    }
    输出结果:

    • 可以看到:在没有使用 NSOperationQueue、在主线程中单独使用使用子类 NSInvocationOperation 执行一个操作的情况下,操作是在当前线程执行的,并没有开启新线程。
    如果在其他线程中执行操作,则打印结果为其他线程。
    // 在其他线程使用子类 NSInvocationOperation
    [NSThread detachNewThreadSelector:@selector(useInvocationOperation) toTarget:self withObject:nil];

    • 可以看到:在其他线程中单独使用子类 NSInvocationOperation,操作是在当前调用的其他线程执行的,并没有开启新线程。

    下边再来看看 NSBlockOperation。


    4.1.2 使用子类 NSBlockOperation

    /**
    * 使用子类 NSBlockOperation
    */

    - (void)useBlockOperation {

    // 1.创建 NSBlockOperation 对象
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];

    // 2.调用 start 方法开始执行操作
    [op start];
    }

    • 可以看到:在没有使用 NSOperationQueue、在主线程中单独使用 NSBlockOperation 执行一个操作的情况下,操作是在当前线程执行的,并没有开启新线程。

    注意:和上边 NSInvocationOperation 使用一样。因为代码是在主线程中调用的,所以打印结果为主线程。如果在其他线程中执行操作,则打印结果为其他线程。

    但是,NSBlockOperation 还提供了一个方法 addExecutionBlock:,通过 addExecutionBlock: 就可以为 NSBlockOperation 添加额外的操作。这些操作(包括 blockOperationWithBlock 中的操作)可以在不同的线程中同时(并发)执行。只有当所有相关的操作已经完成执行时,才视为完成。

    如果添加的操作多的话,blockOperationWithBlock: 中的操作也可能会在其他线程(非当前线程)中执行,这是由系统决定的,并不是说添加到 blockOperationWithBlock: 中的操作一定会在当前线程中执行。(可以使用 addExecutionBlock: 多添加几个操作试试)。


    /**
    * 使用子类 NSBlockOperation
    * 调用方法 AddExecutionBlock:
    */

    - (void)useBlockOperationAddExecutionBlock {

    // 1.创建 NSBlockOperation 对象
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];

    // 2.添加额外的操作
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"4---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"5---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"6---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"7---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"8---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];

    // 3.调用 start 方法开始执行操作
    [op start];
    }

    /**
    * 使用子类 NSBlockOperation
    * 调用方法 AddExecutionBlock:
    */

    - (void)useBlockOperationAddExecutionBlock {

    // 1.创建 NSBlockOperation 对象
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];

    // 2.添加额外的操作
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"4---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"5---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"6---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"7---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"8---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];

    // 3.调用 start 方法开始执行操作
    [op start];
    }

    • 可以看出:使用子类 NSBlockOperation,并调用方法 AddExecutionBlock: 的情况下,blockOperationWithBlock:方法中的操作 和 addExecutionBlock: 中的操作是在不同的线程中异步执行的。而且,这次执行结果中 blockOperationWithBlock:方法中的操作也不是在当前线程(主线程)中执行的。从而印证了blockOperationWithBlock: 中的操作也可能会在其他线程(非当前线程)中执行。

    一般情况下,如果一个 NSBlockOperation 对象封装了多个操作。NSBlockOperation 是否开启新线程,取决于操作的个数。如果添加的操作的个数多,就会自动开启新线程。当然开启的线程数是由系统来决定的。

    4.1.3 使用自定义继承自 NSOperation 的子类

    如果使用子类 NSInvocationOperation、NSBlockOperation 不能满足日常需求,我们可以使用自定义继承自 NSOperation 的子类。可以通过重写 main 或者 start 方法 来定义自己的 NSOperation 对象。重写main方法比较简单,我们不需要管理操作的状态属性 isExecuting 和 isFinished。当 main 执行完返回的时候,这个操作就结束了。

    先定义一个继承自 NSOperation 的子类,重写main方法。

    // YSCOperation.h 文件
    #import <Foundation/Foundation.h>

    @interface YSCOperation : NSOperation

    @end

    // YSCOperation.m 文件
    #import "YSCOperation.h"

    @implementation YSCOperation

    - (void)main {
    if (!self.isCancelled) {
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2];
    NSLog(@"1---%@", [NSThread currentThread]);
    }
    }
    }

    @end
    /**
    * 使用自定义继承自 NSOperation 的子类
    */

    - (void)useCustomOperation {
    // 1.创建 YSCOperation 对象
    YSCOperation *op = [[YSCOperation alloc] init];
    // 2.调用 start 方法开始执行操作
    [op start];
    }


    • 可以看出:在没有使用 NSOperationQueue、在主线程单独使用自定义继承自 NSOperation 的子类的情况下,是在主线程执行操作,并没有开启新线程。

    下边我们来讲讲 NSOperationQueue 的创建。

    4.2 创建队列

    NSOperationQueue 一共有两种队列:主队列、自定义队列。其中自定义队列同时包含了串行、并发功能。下边是主队列、自定义队列的基本创建方法和特点。

    • 主队列
      • 凡是添加到主队列中的操作,都会放到主线程中执行(注:不包括操作使用addExecutionBlock:添加的额外操作,额外操作可能在其他线程执)。


    // 主队列获取方法
    NSOperationQueue *queue = [NSOperationQueue mainQueue];

    • 自定义队列(非主队列)
      • 添加到这种队列中的操作,就会自动放到子线程中执行。
      • 同时包含了:串行、并发功能。
    // 自定义队列创建方法
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    4.3 将操作加入到队列中

    上边我们说到 NSOperation 需要配合 NSOperationQueue 来实现多线程。

    那么我们需要将创建好的操作加入到队列中去。总共有两种方法:

    1. - (void)addOperation:(NSOperation *)op;
      • 需要先创建操作,再将创建好的操作加入到创建好的队列中去。
    /**
    * 使用 addOperation: 将操作加入到操作队列中
    */

    - (void)addOperationToQueue {

    // 1.创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    // 2.创建操作
    // 使用 NSInvocationOperation 创建操作1
    NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];

    // 使用 NSInvocationOperation 创建操作2
    NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task2) object:nil];

    // 使用 NSBlockOperation 创建操作3
    NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op3 addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"4---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];

    // 3.使用 addOperation: 添加所有操作到队列中
    [queue addOperation:op1]; // [op1 start]
    [queue addOperation:op2]; // [op2 start]
    [queue addOperation:op3]; // [op3 start]
    }
    • 使用 NSOperation 子类创建操作,并使用 addOperation: 将操作加入到操作队列后能够开启新线程,进行并发执行。
    1. - (void)addOperationWithBlock:(void (^)(void))block;
      • 无需先创建操作,在 block 中添加操作,直接将包含操作的 block 加入到队列中。


    /**
    * 使用 addOperationWithBlock: 将操作加入到操作队列中
    */


    - (void)addOperationWithBlockToQueue {
    // 1.创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    // 2.使用 addOperationWithBlock: 添加操作到队列中
    [queue addOperationWithBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [queue addOperationWithBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [queue addOperationWithBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    }
    使用 addOperationWithBlock: 将操作加入到操作队列后能够开启新线程,进行并发执行


    作者:Cooci
    链接:https://www.jianshu.com/p/5ee0aa045127








    收起阅读 »

    iOS标准库中常用数据结构和算法之cache

    📝缓存Cache缓存是以键值对的形式进行数据的存储和检索,内部采用哈希表实现。当系统出现内存压力时则会释放掉部分缓存的键值对。 iOS系统提供了一套基于OC语言的高级缓存库NSCache,同时也提供一套基于C语言实现的缓存库libcache.dylib,其中N...
    继续阅读 »

    📝缓存Cache

    缓存是以键值对的形式进行数据的存储和检索,内部采用哈希表实现。当系统出现内存压力时则会释放掉部分缓存的键值对。 iOS系统提供了一套基于OC语言的高级缓存库NSCache,同时也提供一套基于C语言实现的缓存库libcache.dylib,其中NSCache是基于libcache.dylib实现的高级类库,并且这两个库都是线程安全的。 本文主要介绍基于C语言的缓存库的各种API函数。

    头文件: #include <cache.h>, #include <cache_callbacks.h>
    平台: iOS系统

    一、缓存对象的创建和关闭

    功能:创建或者销毁一个缓存对象。

    函数签名:

    int cache_create(const char *name, cache_attributes_t *attrs, cache_t **cache_out);
    int cache_destroy(cache_t *cache);

    参数:

    name:[in] 创建缓存时用来指定缓存的字符串名称,不能为空。
    attrs: [in] 设置缓存的属性。不能为空。
    cache_out: [out] 返回创建的缓存对象。
    return: [out] 成功操作返回0,否则返回非0

    描述:

    缓存对象是一个容器对象,其缓存的内容是一个个键值对,至于这些键值对是什么类型的数据,如何控制键值对的数据的生命周期,如何判断两个键是否是相同的键等等这些信息缓存对象本身是无法得知,因此需要我们明确的告诉缓存对象如何去操作这些键值信息。这也就是为什么在创建缓存对象时需要指定属性这个参数了。属性的参数类型是一个cache_attributes_t结构体。这个结构体的大部分数据成员都是函数指针,这些函数指针就是用来实现对键值进行操作的各种策略。

    struct cache_attributes_s {
    uint32_t version; //缓存对象的版本信息
    cache_key_hash_cb_t key_hash_cb; //对键执行hash计算的函数,不能为空
    cache_key_is_equal_cb_t key_is_equal_cb; //判断两个键是否相等的函数,不能为空

    cache_key_retain_cb_t key_retain_cb; //键加入缓存时调用,用于增加键的引用计数或者进行内存拷贝。
    cache_release_cb_t key_release_cb; //键的释放处理函数,用于对键的内存管理使用。
    cache_release_cb_t value_release_cb; //值的释放处理函数,用于对值的内存管理使用。

    cache_value_make_nonpurgeable_cb_t value_make_nonpurgeable_cb; //当值的引用计数从0变为1时对值内存进行非purgeable的处理函数。
    cache_value_make_purgeable_cb_t value_make_purgeable_cb; //当值的引用计数变为0时对值进行purgeable的处理函数。这个函数的作用是为了解决当内存吃紧时自动释放所分配的内存。

    void *user_data; //附加数据,这个附加数据会在所有的这些回调函数中出现。

    // Added in CACHE_ATTRIBUTES_VERSION_2
    cache_value_retain_cb_t value_retain_cb; //值增加引用计数的函数,用于对值的内存管理使用。
    };
    typedef struct cache_attributes_s cache_attributes_t;

    上述的各种回调函数的格式都在cache.h中有明确的定义,因此这里就不再展开介绍了。一般情况下我们通常都会将字符串或者整数来作为键使用,因此当你采用字符串或者整数作为键时,系统预置了一系列缓存对象属性的默认实现函数。这些函数的声明在cache_callbacks.h文件中

    /*
    * Pre-defined callback functions.
    */

    //用于键进行哈希计算的预置函数
    CACHE_PUBLIC_API uintptr_t cache_key_hash_cb_cstring(void *key, void *unused);
    CACHE_PUBLIC_API uintptr_t cache_key_hash_cb_integer(void *key, void *unused);
    //用于键进行相等比较的预置函数
    CACHE_PUBLIC_API bool cache_key_is_equal_cb_cstring(void *key1, void *key2, void *unused);
    CACHE_PUBLIC_API bool cache_key_is_equal_cb_integer(void *key1, void *key2, void *unused);

    //键值进行释放的函数,这函数默认实现就是调用free函数,因此如果采用这个函数进行释放处理则键值需要从堆中进行内存分配。
    CACHE_PUBLIC_API void cache_release_cb_free(void *key_or_value, void *unused);

    //对值进行purgeable处理的预置函数。
    CACHE_PUBLIC_API void cache_value_make_purgeable_cb(void *value, void *unused);
    CACHE_PUBLIC_API bool cache_value_make_nonpurgeable_cb(void *value, void *unused);

    示例代码:

    //下面代码用于创建一个以字符串为键的缓存对象,其中的缓存对象的属性中的各个成员函数采用的是系统默认预定的函数。
    #include <cache.h>
    #include <cache_callbcaks.h>

    cache_t *im_cache;
    cache_attributes_t attrs = {
    .version = CACHE_ATTRIBUTES_VERSION_2,
    .key_hash_cb = cache_key_hash_cb_cstring,
    .key_is_equal_cb = cache_key_is_equal_cb_cstring,
    .key_retain_cb = my_copy_string,
    .key_release_cb = cache_release_cb_free,
    .value_release_cb = cache_release_cb_free,
    };
    cache_create("com.acme.im_cache", &attrs, &im_cache);

    二、缓存对象中键值对的设置和获取以及删除

    功能:用于处理键值对在缓存中的添加、获取和删除操作。
    函数签名:

    //将键值对添加到缓存,或者替换掉原有的键值对。
    int cache_set_and_retain(cache_t *cache, void *key, void *value, size_t cost);

    //从缓存中根据键获取值
    int cache_get_and_retain(cache_t *cache, void *key, void **value_out);

    //将缓存中的值引用计数减1,当引用计数为0时则清理值分配的内存或者销毁值分配的内存。
    int cache_release_value(cache_t *cache, void *value);

    //从缓存中删除键值。
    int cache_remove(cache_t *cache, void *key);

    参数:
    cache:[in] 缓存对象。
    key:[in] 添加或者获取或者删除时的键。
    cost:[in] 添加缓存时的成本代价,值越大键值在缓存中保留的时间就越长久。
    value:[in] 添加时的值。
    value_out: [out] 用于值获取时的输出。

    描述:
    1、cache_set_and_retain 函数用于将键值对放入缓存中,并指定cost值。当将一个键添加到缓存时,系统内部分别会调用缓存属性cache_attributes_t结构体中的key_retain_cb来实现对键的内存的管理,如果这个函数设置为NULL的话那就表明我们需要自己负责键的生命周期的管理。因为缓存对象内部是通过哈希表来进行数据的存储和检索的,所以在将键值对加入缓存时,还需要提供对键进哈希计算和比较的属性函数key_hash_cb,key_is_equal_cb。 而对于值来说,当值加入缓存时系统会将值的引用计数设置为1,如果我们想自行处理值在缓存中的内存保存则需要指定缓存属性中的value_retain_cb来实现。加入缓存中的值是可以为NULL的。最后的cost参数用于指定这个键值对的成本值,值越小在缓存中保留的时间就越少,反之亦然。

    2、cache_get_and_retain函数用来根据键获取对应的值,如果缓存中没有保存对应的键值对,或者键值对被丢弃,或者值所分配的内存被清除则value_out返回NULL,并且函数返回特殊的值ENOENT。每调用一次值的获取,缓存对象都会增加值的引用计数。因此当我们不再需要访问返回的值时则需要调用手动调用cache_release_value函数来减少缓存对象中值的引用计数。而当值的引用计数变为0时则值分配的内存会设置为可清理(purgeable)或者被销毁掉。

    3、cache_remove函数用于删除缓存中的键值对。当删除缓存中的键值对时,缓存对象会调用属性结构体中的key_release_cb函数进行键的内存销毁,以及如果值的引用计数变为0时则会调用value_release_cb函数进行值的内存销毁。

    示例代码:

    #include <cache.h>
    #include <cache_callbacks.h>

    void main()
    {
    cache_attributes_t attr;
    attr.key_hash_cb = cache_key_hash_cb_cstring;
    attr.key_is_equal_cb = cache_key_is_equal_cb_cstring;
    attr.key_retain_cb = NULL;
    attr.key_release_cb = cache_release_cb_free;
    attr.version = CACHE_ATTRIBUTES_VERSION_2;
    attr.user_data = NULL;
    attr.value_retain_cb = NULL;
    attr.value_release_cb = cache_release_cb_free;
    attr.value_make_purgeable_cb = cache_value_make_purgeable_cb;
    attr.value_make_nonpurgeable_cb = cache_value_make_nonpurgeable_cb;

    //创建缓存
    cache_t *cache = NULL;
    int ret = cache_create("com.test", &attr, &cache);

    //将键值对放入缓存
    char *key = malloc(4);
    strcpy(key, "key");
    char *val = malloc(4);
    strcpy(val, "val");
    ret = cache_set_and_retain(cache, key, val, 0);
    ret = cache_release_value(cache, val);
    //获取键值对,使用后要释放。
    char *val2 = NULL;
    ret = cache_get_and_retain(cache, key, (void**)&val2);
    ret = cache_release_value(cache, val2);

    //删除键值
    cache_remove(cache, key);

    //销毁缓存。
    cache_destroy(cache);
    }

    三、缓存中键值对的清理策略和值的访问策略

    缓存的作用是会对保存在里面的键值对进行丢弃,这取决于当前内存的使用情况以及其他一些场景。在调用cache_set_and_retain将键值对添加到缓存时还会指定一个cost值,值越大被丢弃的可能性就越低。在上面的介绍中有说明缓存会对键值对中的值进行引用计数管理。当调用cache_set_and_retain时值引用计数将设置为1,调用cache_get_and_retain函数获取值时如果键值对在缓存中则会增加值的引用计数。而不需要访问和操作值时我们需要调用cache_release_value函数来将引用计数减1。当值的引用计数变为0时就会立即或者以后发生如下的事情:

    1、如果我们在缓存的属性结构体中设置了value_make_purgeable_cb函数则会调用这个函数表明此时值是可以被清理的。被清理的意思是说为值分配的物理内存随时有可能会被回收。因此当值被设置为可被清理状态时就不能继续去直接访问值所分配的内存了。
    2、如果在此之前键值对因为函数cache_remove的调用而被从缓存中删除,则会调用属性结构体中的value_release_cb函数来执行值内存的销毁处理。
    3、如果因为系统内存的压力导致需要丢弃缓存中的键值对时,就会把值引用计数为0的键值对丢弃掉!注意:只有值引用计数为0时才会缓存被丢弃。

    每次对缓存中的值的访问都需要通过cache_get_and_retain函数来执行,当调用cache_get_and_retain函数时会发生如下事情:

    1、判断当前键值对是否在缓存中,如果不再则值返回NULL。
    2、如果键值对在缓存中。并且值的引用计数为0,就会判断缓存的结构体属性中是否存在value_make_nonpurgeable_cb函数,如果存在则会调用value_make_nonpurgeable_cb函数将值的内存设置为不可清理,如果设置为不可清理返回为false则表明此时值的内存已经被清理了,这时候键值对将会从缓存中丢弃,并且cache_get_and_retain函数将返回值为NULL。当然如果value_make_nonpurgeable_cb函数为空则不会发生这一步。
    3、增加值的引用计数,并返回值。

    链接:https://www.jianshu.com/p/2f58f165bf1a

    收起阅读 »

    iOS开发 - 面试被问到内存概念怎么办?

    在早期的计算机中,程序是直接运行在物理内存上的,也就是说:程序在运行时访问的地址就是物理地址。这样也就是单运行的时候没有什么问题!可是,计算机会有多到程序、分时系统和多任务,当我们能够同时运行多个程序时,CPU的利用率将会比较高。那么有一个非常严重的问题:如何...
    继续阅读 »
    在早期的计算机中,程序是直接运行在物理内存上的,也就是说:程序在运行时访问的地址就是物理地址。这样也就是单运行的时候没有什么问题!可是,计算机会有多到程序、分时系统和多任务,当我们能够同时运行多个程序时,CPU的利用率将会比较高。那么有一个非常严重的问题:如何将计算机的有限的物理内存分配给多个程序使用

    假设我们计算有128MB内存,程序A需要10MB,程序B需要100MB,程序C需要20MB。如果我们需要同时运行程序A和B,那么比较直接的做法是将内存的前10MB分配给程序A,10MB~110MB分配给B。




    但这样做,会造成以下问题:

    • 当多个程序需要运行时,必须保证这些程序用到的内存总量要小于计算机实际的物理内存的大小。

    • 进程地址空间不隔离,由于程序是直接访问物理内存的,所以每一个进程都可以修改其他进程的内存数据,设置修改内核地址空间中的数据,所以有些恶意程序可以随意修改别的进程,就会造成一些破坏

    • 内存使用效率低 内存空间不足,就需要将其他程序展示拷贝到硬盘当中,然后将新的程序装入内存。然而由于大量的数据装入装出,内存的使用效率会非常低

    • 程序运行的地址不确定;因为内存地址是随机分配的,所以程序运行的地址也是不正确的

    解决这几个问题的思路就是使用我们非常牛逼的方法:增加中间层 - 即使用一种间接的地址访问方式。


    把程序给出的地址看做是一种虚拟地址,然后通过某种映射,将这个虚拟地址转化到实际的物理地址。这样,只需要控制好映射过程,就能保证程序所能访问的物理内存区域跟别的程序不重叠,达到空间隔离的效果。

    隔离

    普通的程序它只需要一个简单的执行环境一个单一的地址空间有自己的CPU
    地址空间比较抽象,如果把它想象成一个数组,每一个数组是一字节,数组大小就是地址空间的长度,那么32位的地址空间大小就是2^32=4294967296字节,即4G,地址空间有效位是0x00000000~0xFFFFFFFF
    地址空间分为两种:

    • 物理空间:就是物理内存。32位的机器,地址线就有32条,物理空间4G,但如果只装有512M的内存,那么实际有效的空间地址就是0x00000000~0x1FFFFFFF,其他部分都是无效的。

    • 虚拟空间:每个进程都有自己独立的虚拟空间,而且每个进程只能访问自己的空间地址,这样就有效的做到了进程隔离。

    分段

    基本思路: 把一段与程序所需要的内存空间大小的虚拟空间映射到某个地址空间。虚拟空间的每个字节对应物理空间的每个字节。这个映射过程由软件来完成。

    比如A需要10M,就假设有0x00000000 到0x00A00000大小的虚拟空间,然后从物理内存分配一个相同大小的空间,比如是0x001000000x00B00000。操作系统来设置这个映射函数,实际的地址转换由硬件完成。如果越界,硬件就会判断这是一个非法访问,拒绝这个地址请求,并上报操作系统或监控程序。





    这样一来利用分段的方式可以解决之前的地址空间不隔离程序运行地址不确定

    • 首先做到了地址隔离,因为A和B被映射到了两块不同的物理空间,它们之间没有任何重叠,如果A访问虚拟空间的地址超过了0x00A00000这个范围,硬件就会判断这是一个非法的访问,并将这个请求报告给操作系统或者监控程序,由它决定如何处理。

    • 再者,对于每个程序来说,无论它们被分配到地址空间的哪一个区域,对于程序来说都是透明的,它们不需要关心物理地址的变化,它们只要按照从地址0x000000000x00A00000来编写程序、放置变量,所以程序不需要重定位。

    第二问题内存使用效率问题依旧没有解决。

    但是分段的方法没有解决内存使用效率的问题。分段对于内存区域的映射还是按照程序为单位,如果内存不足,被换入换出的磁盘的都是整个程序,这样势必会造成大量的磁盘访问操作,从而严重影响速度,这种方法还是显得粗糙,粒度比较大。事实上根据程序的局部性原理,当一个程序正在运行时,在某个时间段内,它只是频繁用到了一小部分数据,也就是说,程序的很多数据其实在一个时间段内是不会被用到的。人们很自然地想到了更小粒度的内存分割和映射方法,使得程序的局部性原理得到充分利用,大大提高了内存的使用率。这种方法就是分页。

    分页

    分页的基本方法是把地址空间人为得等分成固定大小的页,每一个页的大小由硬件决定,或硬件支持多种页的大小,由操作系统选择决定页的大小。 目前几乎所有PC的操作系统都是用4KB大小的页。我们使用的PC机是32位虚拟地址空间,也就是4GB,按4KB分页,总共有1048576个页。

    那么,当我们把进程的虚拟地址空间按页分割,把常用的数据和代码装载到内存中,把不常用的代码和数据保存在磁盘里,当需要用到的时候再把它们从磁盘里取出即可。图中的线表示映射关系,我们可以看到虚拟空间有些页被映射到同一个物理页,这样就可以实现内存共享。
    虚拟页,物理页,磁盘页根据内存空间不一样而区分

    我们可以看到Process 1 的VP2和VP3不在内存中,但是当进程需要用到这两个页的时候,硬件就会捕获到这个消息,就是所谓的页错误(Page Fault),然后操作系统接管进程,负责将VP2和VP3从磁盘读取出来装入内存,然都将内存中的这两个页和VP2和VP3建立映射关系。以页为单位存取和交换数据非常方便,硬件本身就支持这种以页为单位的操作方式。


  • 保护页也是页映射的目的之一,简单地说就是每个页可以设置权限属性,谁可以修改,谁可以访问,而且只有操作系统有权修改这些属性,那么操作系统就可以做到保护自己和保护进程。

  • 虚拟存储的实现需要硬件支持,几乎所有CPU都采用称为MMU的部件来进行页的映射:




  • 在页映射模式下,CPU发出的是Virtual Address,即我们程序看到的是虚拟地址。经过MMU转换以后就变成了Physical Address。一般MMU集成在CPU内部,不会以独立的部件存在。

    作者:Cooci
    链接:https://www.jianshu.com/p/1ad04daa1b8a











    收起阅读 »

    多线程安全-iOS开发注意咯

    正式因为多线程能够在时间片里被CPU快速切换,造就了以下优势资源利用率更好程序设计在某些情况下更简单程序响应更快但是并不是非常完美,因为多线程常常伴有资源抢夺的问题,作为一个高级开发人员并发编程那是必须要的,同时解决线程安全也成了我们必须要要掌握的基础原子操作...
    继续阅读 »



    正式因为多线程能够在时间片里被CPU快速切换,造就了以下优势

    • 资源利用率更好
    • 程序设计在某些情况下更简单
    • 程序响应更快

    但是并不是非常完美,因为多线程常常伴有资源抢夺的问题,作为一个高级开发人员并发编程那是必须要的,同时解决线程安全也成了我们必须要要掌握的基础


    原子操作

    自旋锁其实就是封装了一个spinlock_t自旋锁

    自旋锁:如果共享数据已经有其他线程加锁了,线程会以死循环的方式等待锁,一旦被访问的资源被解锁,则等待资源的线程会立即执行。自旋锁下面还会展开来介绍

    互斥锁:如果共享数据已经有其他线程加锁了,线程会进入休眠状态bool lock = false; // 一开始没有锁上,任何线程都可以申请锁

    do {
    while(test_and_set(&lock); // test_and_set 是一个原子操作
    Critical section // 临界区
    lock = false; // 相当于释放锁,这样别的线程可以进入临界区
    Reminder section // 不需要锁保护的代码
    }


    操作在底层会被编译为汇编代码之后不止一条指令,因此在执行的时候可能执行了一半就被调度系统打断,去执行别的代码,而我们的原子性的单条指令的执行是不会被打断的,所以保证了安全.

    自旋锁的BUG

    尽管原子操作非常的简单,但是它只适合于比较简单特定的场合。在复杂的场合下,比如我们要保证一个复杂的数据结构更改的原子性,原子操作指令就力不从心了,

    如果临界区的执行时间过长,使用自旋锁不是个好主意。之前我们介绍过时间片轮转算法,线程在多种情况下会退出自己的时间片。其中一种是用完了时间片的时间,被操作系统强制抢占。除此以外,当线程进行 I/O 操作,或进入睡眠状态时,都会主动让出时间片。显然在 while 循环中,线程处于忙等状态,白白浪费 CPU 时间,最终因为超时被操作系统抢占时间片。如果临界区执行时间较长,比如是文件读写,这种忙等是毫无必要的

    下面开始我们又爱又恨的


    iOS锁

    锁并是一种非强制机制,每一个现货出呢个在访问数据或资源之前视图获取(Acquire)锁,并在访问结束之后释放(Release)锁。在锁已经被占用的时候试图获取锁,线程会等待,知道锁重新可用!

    信号量

    二元信号量(Binary Semaphore)只有两种状态:占用与非占用。它适合被唯一一个线程独占访问的资源。当二元信号量处于非占用状态时,第一个试图获取该二元信号量的线程会获得该锁,并将二元信号量置为占用状态,伺候其他的所有试图获取该二元信号量的线程将会等待,直到该锁被释放

    现在我们在这个基础上,我们把学习的思维由二元->多元的时候,我们的信号量由此诞生,多元信号量简称信号量

    • 将信号量的值减1

    • 如果信号量的值小于0,则进入等待状态,否则继续执行。访问玩资源之后,线程释放信号量,进行如下操作

    • 将信号量的值加1

    • 如果信号量的值小于1,唤醒一个等待中的线程


    let sem = DispatchSemaphore(value: 1)

    for index in 1...5 {
    DispatchQueue.global().async {
    sem.wait()
    print(index,Thread.current)
    sem.signal()
    }
    }

    输出结果:
    1 <NSThread: 0x600003fa8200>{number = 3, name = (null)}
    2 <NSThread: 0x600003f90140>{number = 4, name = (null)}
    3 <NSThread: 0x600003f94200>{number = 5, name = (null)}
    4 <NSThread: 0x600003fa0940>{number = 6, name = (null)}
    5 <NSThread: 0x600003f94240>{number = 7, name = (null)}

    互斥量


    互斥量(Mutex)又叫互斥锁和二元信号量很类似,但和信号量不同的是,信号量在整个系统可以被任意线程获取并释放;也就是说哪个线程锁的,要哪个线程释放锁。

    Mutex可以分为递归锁(recursive mutex)非递归锁(non-recursive mutex)。 递归锁也叫可重入锁(reentrant mutex),非递归锁也叫不可重入锁(non-reentrant mutex)
    二者唯一的区别是:
    • 同一个线程可以多次获取同一个递归锁,不会产生死锁。
    • 如果一个线程多次获取同一个非递归锁,则会产生死锁。

    NSLock 是最简单额互斥锁!但是是非递归的!直接封装了pthread_mutex 用法非常简单就不做赘述
    @synchronized 是我们互斥锁里面用的最频繁的,但是性能最差!

    int main(int argc, const char * argv[]) {
    NSString *obj = @"Iceberg";
    @synchronized(obj) {
    NSLog(@"Hello,world! => %@" , obj);
    }
    }

    底层clang

    int main(int argc, const char * argv[]) {

    NSString *obj = (NSString *)&__NSConstantStringImpl__var_folders_8l_rsj0hqpj42b9jsw81mc3xv_40000gn_T_block_main_54f70c_mi_0;

    {
    id _rethrow = 0;
    id _sync_obj = (id)obj;
    objc_sync_enter(_sync_obj);
    try {
    struct _SYNC_EXIT {
    _SYNC_EXIT(id arg) : sync_exit(arg) {}
    ~_SYNC_EXIT() {
    objc_sync_exit(sync_exit);
    }
    id sync_exit;
    } _sync_exit(_sync_obj);

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_8l_rsj0hqpj42b9jsw81mc3xv_40000gn_T_block_main_54f70c_mi_1 , obj);

    } catch (id e) {
    _rethrow = e;
    }

    {
    struct _FIN {
    _FIN(id reth) : rethrow(reth) {}
    ~_FIN() {
    if (rethrow)
    objc_exception_throw(rethrow);
    }
    id rethrow;
    } _fin_force_rethow(_rethrow);
    }
    }

    }
    我们发现objc_sync_enter函数是在try语句之前调用,参数为需要加锁的对象。因为C++中没有try{}catch{}finally{}语句,所以不能在finally{}调用objc_sync_exit函数。因此objc_sync_exit是在_SYNC_EXIT结构体中的析构函数中调用,参数同样是当前加锁的对象。这个设计很巧妙,原因在_SYNC_EXIT结构体类型的_sync_exit是一个局部变量,生命周期为try{}语句块,其中包含了@sychronized{}代码需要执行的代码,在代码完成后,_sync_exit局部变量出栈释放,随即调用其析构函数,进而调用objc_sync_exit函数。即使try{}语句块中的代码执行过程中出现异常,跳转到catch{}语句,局部变量_sync_exit同样会被释放,完美的模拟了finally的功能。


    int objc_sync_enter(id obj)
    {
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {
    SyncData* data = id2data(obj, ACQUIRE);
    require_action_string(data != NULL, done, result = OBJC_SYNC_NOT_INITIALIZED, "id2data failed");

    result = recursive_mutex_lock(&data->mutex);
    require_noerr_string(result, done, "mutex_lock failed");
    } else {
    // @synchronized(nil) does nothing
    if (DebugNilSync) {
    _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
    }
    objc_sync_nil();
    }

    done:
    return result;
    }


    从上面的源码中我们可以得出你调用sychronized的每个对象,Objective-C runtime都会为其分配一个递归锁并存储在哈希表中。完美

    其实如果大家觉得@sychronized性能低的话,完全可以用NSRecursiveLock现成的封装好的递归锁

    NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    static void (^RecursiveBlock)(int);
    RecursiveBlock = ^(int value) {
    [lock lock];
    if (value > 0) {
    NSLog(@"value:%d", value);
    RecursiveBlock(value - 1);
    }
    [lock unlock];
    };
    RecursiveBlock(2);
    });

    2016-08-19 14:43:12.327 ThreadLockControlDemo[1878:145003] value:2
    2016-08-19 14:43:12.327 ThreadLockControlDemo[1878:145003] value:1

    条件变量

    条件变量(Condition Variable)作为一种同步手段,作用类似一个栅栏。对于条件变量,现成可以有两种操作:

    • 首先线程可以等待条件变量,一个条件变量可以被多个线程等待
    • 其次线程可以唤醒条件变量。此时某个或所有等待此条件变量的线程都会被唤醒并继续支持。

    换句话说:使用条件变量可以让许多线程一起等待某个时间的发生,当某个时间发生时,所有的线程可以一起恢复执行!

    相信仔细的大家肯定在锁的用法里面见过NSCondition,就是封装了条件变量pthread_cond_t和互斥锁

    - (void) signal { 
    pthread_cond_signal(&_condition);
    }
    // 其实这个函数是通过宏来定义的,展开后就是这样
    - (void) lock {
    int err = pthread_mutex_lock(&_mutex);
    }
    NSConditionLock借助 NSCondition来实现,它的本质就是一个生产者-消费者模型。“条件被满足”可以理解为生产者提供了新的内容。NSConditionLock 的内部持有一个NSCondition对象,以及 _condition_value属性,在初始化时就会对这个属性进行赋值:

    // 简化版代码
    - (id) initWithCondition: (NSInteger)value {
    if (nil != (self = [super init])) {
    _condition = [NSCondition new]
    _condition_value = value;
    }
    return self;
    }

    临界区

    比互斥量更加严格的同步手段。在术语中,把临界区的获取称为进入临界区,而把锁的释放称为离开临界区。与互斥量和信号量的区别:

    • (1)互斥量和信号量字系统的任何进程都是可见的。
    • (2)临界区的作用范围仅限于本进程,其他进程无法获取该锁。

    // 临界区结构对象
    CRITICAL_SECTION g_cs;
    // 共享资源
    char g_cArray[10];
    UINT ThreadProc10(LPVOID pParam)
    {
    // 进入临界区
    EnterCriticalSection(&g_cs);
    // 对共享资源进行写入操作
    for (int i = 0; i < 10; i++)
    {
    g_cArray[i] = a;
    Sleep(1);
    }
    // 离开临界区
    LeaveCriticalSection(&g_cs);
    return 0;
    }
    UINT ThreadProc11(LPVOID pParam)
    {
    // 进入临界区
    EnterCriticalSection(&g_cs);
    // 对共享资源进行写入操作
    for (int i = 0; i < 10; i++)
    {
    g_cArray[10 - i - 1] = b;
    Sleep(1);
    }
    // 离开临界区
    LeaveCriticalSection(&g_cs);
    return 0;
    }
    ……
    void CSample08View::OnCriticalSection()
    {
    // 初始化临界区
    InitializeCriticalSection(&g_cs);
    // 启动线程
    AfxBeginThread(ThreadProc10, NULL);
    AfxBeginThread(ThreadProc11, NULL);
    // 等待计算完毕
    Sleep(300);
    // 报告计算结果
    CString sResult = CString(g_cArray);
    AfxMessageBox(sResult);
    }

    读写锁

    int pthread_rwlock_rdlock(pthread_rwlock_t *rwptr);
    int pthread_rwlock_wrlock(pthread_rwlock_t *rwptr);
    int pthread_rwlock_unlock(pthread_rwlock_t *rwptr);

    ReadWriteLock管理一组锁,一个是只读的锁,一个是写锁。读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。


    #include <pthread.h>      //多线程、读写锁所需头文件
    pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; //定义和初始化读写锁

    写模式:
    pthread_rwlock_wrlock(&rwlock); //加写锁
    写写写……
    pthread_rwlock_unlock(&rwlock); //解锁

    读模式:
    pthread_rwlock_rdlock(&rwlock); //加读锁
    读读读……
    pthread_rwlock_unlock(&rwlock); //解锁


    • 用条件变量实现读写锁

    这里用条件变量+互斥锁来实现。注意:条件变量必须和互斥锁一起使用,等待、释放的时候都需要加锁。

    #include <pthread.h> //多线程、互斥锁所需头文件

    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //定义和初始化互斥锁
    pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //定义和初始化条件变量


    写模式:
    pthread_mutex_lock(&mutex); //加锁
    while(w != 0 || r > 0)
    {
    pthread_cond_wait(&cond, &mutex); //等待条件变量的成立
    }
    w = 1;

    pthread_mutex_unlock(&mutex);
    写写写……
    pthread_mutex_lock(&mutex);
    w = 0;
    pthread_cond_broadcast(&cond); //唤醒其他因条件变量而产生的阻塞
    pthread_mutex_unlock(&mutex); //解锁


    读模式:
    pthread_mutex_lock(&mutex);
    while(w != 0)
    {
    pthread_cond_wait(&cond, &mutex); //等待条件变量的成立
    }
    r++;
    pthread_mutex_unlock(&mutex);
    读读读……
    pthread_mutex_lock(&mutex);
    r- -;
    if(r == 0)
    pthread_cond_broadcast(&cond); //唤醒其他因条件变量而产生的阻塞
    pthread_mutex_unlock(&mutex); //解锁
    • 用互斥锁实现读写锁

    这里使用2个互斥锁+1个整型变量来实现

    #include <pthread.h> //多线程、互斥锁所需头文件
    pthread_mutex_t r_mutex = PTHREAD_MUTEX_INITIALIZER; //定义和初始化互斥锁
    pthread_mutex_t w_mutex = PTHREAD_MUTEX_INITIALIZER;
    int readers = 0; //记录读者的个数

    写模式:
    pthread_mutex_lock(&w_mutex);
    写写写……
    pthread_mutex_unlock(&w_mutex);


    读模式:
    pthread_mutex_lock(&r_mutex);

    if(readers == 0)
    pthread_mutex_lock(&w_mutex);
    readers++;
    pthread_mutex_unlock(&r_mutex);
    读读读……
    pthread_mutex_lock(&r_mutex);
    readers- -;
    if(reader == 0)
    pthread_mutex_unlock(&w_mutex);
    pthread_mutex_unlock(&r_mutex);


    • 用信号量来实现读写锁

    这里使用2个信号量+1个整型变量来实现。令信号量的初始数值为1,那么信号量的作用就和互斥量等价了。

    #include <semaphore.h>     //线程信号量所需头文件

    sem_t r_sem; //定义信号量
    sem_init(&r_sem, 0, 1); //初始化信号量

    sem_t w_sem; //定义信号量
    sem_init(&w_sem, 0, 1); //初始化信号量
    int readers = 0;

    写模式:
    sem_wait(&w_sem);
    写写写……
    sem_post(&w_sem);


    读模式:
    sem_wait(&r_sem);
    if(readers == 0)
    sem_wait(&w_sem);
    readers++;
    sem_post(&r_sem);
    读读读……
    sem_wait(&r_sem);
    readers- -;
    if(readers == 0)
    sem_post(&w_sem);
    sem_post(&r_sem);



    线程的安全是现在各个领域在多线程开发必须要掌握的基础!只有对底层有所掌握,才能在真正的实际开发中游刃有余!


    收起阅读 »

    iOS标准库中常用数据结构和算法之内存池

    ⛲️内存池内存池提供了内存的复用和持久的存储功能。设想一个场景,当你分配了一块大内存并且填写了内容,但是你又不是经常去访问这块内存。这样的内存利用率将不高,而且无法复用。而如果是采用内存池则可以很轻松解决这个问题:你只需要从内存池中申请这块内存,设置完内容后当...
    继续阅读 »

    ⛲️内存池

    内存池提供了内存的复用和持久的存储功能。设想一个场景,当你分配了一块大内存并且填写了内容,但是你又不是经常去访问这块内存。这样的内存利用率将不高,而且无法复用。而如果是采用内存池则可以很轻松解决这个问题:你只需要从内存池中申请这块内存,设置完内容后当不需要用时你可以将这块内存放入内存池中,供其他地方在申请时进行复用,而当你再次需要时则只需要重新申请即可。内存池提供了内存分配编号而且设置脏标志的概念,当你把分配的内存放入内存池并设置脏标志后,系统就会在适当的时候将这块内存的内容写回到磁盘,这样当你再次根据内存编号来访问内存时,系统就又会从磁盘中将内容读取到内存中去。

    功能:在iOS中提供了一套内存池管理的API,你可以用这套API来实现上述的功能,而且系统内部很多功能也是借助内存池来实现内存复用和磁盘存储的。
    头文件: #include <mpool.h>, #include <db.h>
    平台: BSD系统,linux系统

    一、内存池的创建、同步和关闭

    功能:创建和关闭一个内存池对象并和磁盘文件绑定以便进行同步操作。
    函数签名:

    //创建一个内存池对象
    MPOOL * mpool_open(void *key, int fd, pgno_t pagesize, pgno_t maxcache);

    //将内存池中的脏数据同步写回到磁盘文件中
    int mpool_sync(MPOOL *mp);

    //关闭和销毁内存池对象。
    int mpool_close(MPOOL *mp);

    参数:

    key:[in] 保留字段,暂时没有用处,传递NULL即可。
    fd:[in] 内存池关联的磁盘文件句柄,文件句柄需要用open函数来打开。
    pagesize:[in] 内存池中每次申请和分配的内存的尺寸大小,单位是字节。
    maxcache:[in] 内存池中内存页的最大缓存数量。如果池中缓存的内存数量超过了最大缓存的数量就会复用已经存在的内存,而不是每次都分配新的内存。
    return:[out] 返回一个新创建的内存池对象,其他两个函数成功返回0,失败返回非0.

    描述:

    1、内存池中的内存的分配和获取是以页为单位进行的,每次分配的页的尺寸大小由pagesize指定,同时内存池也指定了最大的缓存页数量maxcache。每次从内存池中分配一页内存时,除了会返回分配的内存地址外,还会返回这一页内存的编号。这个编号对于内存池中的内存页来说是唯一的。因为内存池中的内存是可以被复用的,因此就有可能是不同的编号的内存页所得到的内存地址是相同的。

    2、每一个内存池对象都会要和一个文件关联起来,以便能够实现内存数据的永久存储和内存的复用。文件句柄必须用open函数来打开,比如下面的例子:

    int fd = open("/Users/apple/mpool", O_RDWR|O_APPEND|O_CREAT,0660);

    3、当我们不需要使用某个内存页时或者内存页的内容有改动则我们需要将这个内存页放入回内存池中,并将页标志为脏(DIRTY)。这样系统就会在适当的时候将此内存页的数据写回到磁盘文件中,同时此内存页也会在后续被重复利用。

    4、当我们想将所有设置为脏标志的内存页立即写入磁盘时则需要调用mpool_sync函数进行同步处理。

    5、当我们不再需要内存池时,则可以通过mpool_close来关闭内存池对象,需要注意的是关闭内存池并不会将内存中的数据回写到磁盘中去。

    二、内存池中内存的获取

    功能: 从内存池中申请分配一页新的内存或者获取现有缓存中的内存。
    函数签名:

    //从内存池中申请分配一页新的内存
    void * mpool_new(MPOOL *mp, pgno_t *pgnoaddr);
    //根据内存编号页获取对应的内存。
    void * mpool_get(MPOOL *mp, pgno_t pgno, u_int flags);

    参数:

    mp:[in] 内存池对象。
    pgnoaddr:[out] 用于mpool_new函数,用于保存新分配的内存页编号。
    pngno:[in] 用于mpool_get函数,指定要获取的内存页的编号。
    flags:[in] 此参数暂时无用。
    return:[out] 返回分配或者获取的内存地址。如果分配或者获取失败则返回NULL。

    描述:

    1、无论是new还是get每次从内存池里面分配或者获取的内存页的大小都是由上述mpool_open函数中的pagesize参数指定的大小。
    2、系统内部分配的内存是用calloc函数实现的,但是我们不需要手动调用free来对内存进行释放处理。
    3、每个内存页都有一个唯一的页编号,而且每次分配的页编号也会一直递增下去。
    4、mpool_new函数申请分配新的内存时,如果当前缓存中的内存页小于maxcache数量则总是分配新的内存,只有当缓存数量大于maxcache时才会从现有的缓存中寻找一页可以被重复利用的内存页,如果没有可以重复利用的页面,则会继续分配新的内存页。
    5、mpool_get函数则根据内存页的编号获取对应的内存页。如果编号不存在则返回NULL。需要注意的是一般在获取了某一页内存后,不要进行重复获取操作,否则在DEBUG状态下会返回异常。另外一个情况是有可能相同的页编号下两次获取的内存地址是不一样的,因为系统实现内部有内存复用的机制。

    三、内存池中内存的放回

    功能:将分配或者申请的内存页放回到内存池中去,以便进行重复利用。
    函数签名:

    int  mpool_put(MPOOL *mp, void *pgaddr, u_int flags);

    参数:

    mp: [in] 内存池对象。
    pgaddr:[in] 要放入缓存的内存页地址。这个地址由mpool_get/new两个函数返回。
    flags:[in] 放回的属性,一般设置为0或者MPOOL_DIRTY。
    return:[in] 函数调用成功返回0,失败返回非0

    描述:

    1、这个函数用来将内存页放入回内存池缓存中,以便对内存进行重复利用。当将某个内存地址放入回缓存后,将不能再次访问这个内存地址了。如果要想继续访问内存中的数据则需要借助上述的mpool_get/new函数来重新获取。
    2、flags:属性如果指定为0时,表明放弃这次内存中的内容的修改,系统不会将内存中的内容写入到磁盘中,而只是将内存放入缓存中供其他地方重复使用。而如果设置为MPOOL_DIRTY时,则表明将这页内存中的数据设置为脏标志,除了同样将内存放入缓存中重复利用外,则会在适当的时候将内存中的数据写入到磁盘中,以便下次进行读取。

    四、内存池磁盘读写通知

    功能:注册回调函数,当某页内存要写回到磁盘或者要从磁盘中读取时就会调用指定的回调函数。
    函数签名:

    void mpool_filter(MPOOL *mp, void (*pgin)(void *, pgno_t, void *),
    void (*pgout)(void *, pgno_t, void *), void *pgcookie);

    参数:

    mp:[in] 内存池对象.
    pgin: [in]: 回调函数,当某个内存页的数据需要从磁盘读取时,会在读取完成后调用这个回调函数。
    pgout:[in]: 回调函数,当某个内存页的数据要到磁盘时,会在写入完成后调用这个回调函数。
    pgcookie: [in] 上述两个回调函数的附加参数。

    描述:

    因为内存池中的内存页会进行复用,以及会在适当的时候将内容同步到磁盘中,或者从磁盘中将内容读取到内存中,因此可以借助这个函数来监控这些磁盘文件和内存之间的读写操作。pgin和pgout函数的格式定义如下:

    //pgin和pgout回调函数的格式。
    //pgcookie:是mpool_filter函数中传入的参数。
    //pgno: 要进行读写的内存页编号
    //pageaddr: 要进行读写的内存地址。
    void (*pgcallback)(void *pgcookie, pgno_t pgno, void *pageaddr);

    五、实例代码

    #include <mpool.h>
    #include <db.h>

    //创建并打开一个文件。
    int fd = open("/Users/apple/mpool", O_RDWR|O_APPEND|O_CREAT,0660);

    //创建一个内存池对象,每页的内存100个字节,最大的缓存数量为4
    MPOOL *pool = mpool_open(NULL, fd, 100, 4);


    //从内存池中分配一个新的内存页,这里对返回的内存填写数据。
    pgno_t pidx1, pidx2 = 0;
    char *mem1 = (char*)mpool_new(pool, &pidx1);
    memcpy(mem1, "aaa", 4);

    char *mem2 = (char*)mpool_new(pool, &pidx2);
    memcpy(mem2, "bbb", 4);

    //将分配的内存mem1放回内存池中,但是内容不保存到磁盘
    mpool_put(pool, mem1, 0);
    //将分配的内存mem2放回内存池中,但是内容保存到磁盘。
    mpool_put(pool, mem2, MPOOL_DIRTY);

    //经过上面的操作后mem1,mem2将不能继续再访问了,需要访问时需要再次调用mpool_get。
    mem1 = (char*)mpool_get(pool, pidx1, 0);
    mem2 = (char*)mpool_get(pool, pidx2, 0);

    //上面的mem1和mem2可能和前面的new返回的地址是不一样的。因此在内存池中不能通过地址来做唯一比较,而应该将编号来进行比较。

    //将所有设置为脏标志的内存也写回到磁盘中去。
    mpool_sync(pool);

    mpool_close(pool); //关闭内存池。

    close(fd); //关闭文件。

    内存池为iOS系统底层开发提供了一个非常重要的能力,我们可以好好利用内存池来对内存进行管理,以及一些需要进行持久化的数据也可以借助内存池来进行保存,通过内存池提高内存的重复利用率。

    转自:https://www.jianshu.com/p/34bd3e5c5b4e

    收起阅读 »

    css 加载阻塞问题,无废话

    一、问题 & 结论1. css 加载会阻塞 DOM 树的解析渲染吗 ?css 并不会阻塞 dom 树的解析css 会阻塞 dom 树的渲染2. css 加载会阻塞 js 运行吗 ?css 加载会阻塞后面 js 语句的执行二、造成的结果以及优化方案1. ...
    继续阅读 »

    一、问题 & 结论

    1. css 加载会阻塞 DOM 树的解析渲染吗 ?
    • css 并不会阻塞 dom 树的解析
    • css 会阻塞 dom 树的渲染
    2. css 加载会阻塞 js 运行吗 ?
    • css 加载会阻塞后面 js 语句的执行

    二、造成的结果以及优化方案

    1. 造成的结果
    • css 加载缓慢会造成长时间的白屏

    2. 优化方案
    1. CDN 加速:CDN 会根据网络状况,挑选一个最近的具有缓存内容的节点提供资源,减少加载时间
    2. 对 css 进行压缩:使用打包工具 webpack、gulp 等,开启 gzip 压缩
    3. 合理的使用缓存:强缓存、协商缓存等策略
    4. 减少 http 请求次数,合并 css 文件,或者干脆写成内联样式(缺点:不能缓存)

    三、原理解析

    1. 浏览器的渲染过程如下图所示:


    2. 结论如下:


    • DOM 解析和 css 解析是两个独立并行的进行,所以 css 的加载不会阻塞 DOM 的解析

    • 由于 Render Tree 是依赖于 DOM Tree 和 CSSOM Tree 的,所以他必须等待到 CSSOM Tree 构建完成,也就是 CSS 资源加载完成(或者 CSS 资源加载失败)后,才能开始渲染。因此,CSS 加载是会阻塞 Dom 的渲染的

    • 由于 js 可能会操作之前的 Dom 节点和 css 样式,因此浏览器会维持 html 中 css 和 js 的顺序。因此,样式表会在后面的 js 执行前先加载执行完毕。所以 css 会阻塞后面 js 的执行

    四、实际场景


    1. 页面加载的两个事件


    • onLoad:等待页面的所有资源都加载完成才会触发,这些资源包括 css、js、图片视频等

    • DOMContentLoaded:就是当页面的内容解析完成后,则触发该事件


    2. css 加载的影响


    • 如果页面中同时存在 css 和 js,并且存在 js 在 css 后面,则 DOMContentLoaded 事件会在 css 加载完后才执行

    • 其他情况下,DOMContentLoaded 都不会等待 css 加载,并且 DOMContentLoaded 事件也不会等待图片、视频等其他资源加载

    链接:https://juejin.cn/post/6967530166520119332





    收起阅读 »

    深入理解CSS中的z-index

    深入理解z-index在MDN上的定义是z-index 属性设定了一个定位元素及其后代元素或 flex 项目的 z-order。 当元素之间重叠的时候, z-index 较大的元素会覆盖较小的元素在上层进行显示。但是这个说明太含糊了,当遇到z-ind...
    继续阅读 »

    深入理解z-index

    MDN上的定义是z-index 属性设定了一个定位元素及其后代元素或 flex 项目的 z-order。 当元素之间重叠的时候, z-index 较大的元素会覆盖较小的元素在上层进行显示。

    但是这个说明太含糊了,当遇到z-index不生效的情况时,就不知所以然了,最近也查看了很多和z-index相关的资料,决定把z-index相关知识系统性的梳理一遍.


    以前我总是很片面的认为元素在Z轴上的层叠顺序只跟z-index值的大小有关, 属性值大的元素显示在上面、属性值小的元素显示在下面,但是就像下面, 为啥z-index不生效呢,明明box1的z-index属性值大于box2的.


    事实上z-index属性并非对所有元素都生效, 它仅对定位元素生效而且定位元素的prosition属性值不为static时才会起作用:

    其实判断元素在Z轴的层叠顺序取决于两个方面: 元素所在的层叠上下文元素自身的层叠级别, 在此之前我们先了解一下这两个概念:

    z-index

    通常情况下,html页面可以被认为是二维的,因为文本,图像和其他元素被排列在页面上而不重叠。在这种情况下,只有一个渲染进程,所有元素都知道其他元素所占用的空间。


    CSS 2.1 中, 所有的盒模型元素都处于三维坐标系中。 除了我们常用的横坐标和纵坐标, 盒模型元素还可以沿着“z 轴”层叠摆放, 当他们相互覆盖时,z轴顺序就变得十分重要。这意味着 CSS 允许你在现有的渲染引擎上层叠的摆放盒模型元素。 所有的层都可以用一个整数( z 轴顺序)来表明当前层在 z 轴的位置。 数字越大, 元素越接近观察者。Z 轴顺序用 CSS 的 z-index 属性来指定。z-index的属性值默认为auto,可设置值为一个整数、可为正整数也可以是负整数


    层叠上下文

    MDN上的定义: 我们假定用户正面向(浏览器)视窗或网页,而 HTML 元素沿着其相对于用户的一条虚构的z轴排开,层叠上下文就是对这些HTML元素的一个三维构想。众HTML元素基于其元素属性按照优先级顺序占据这个空间。


    那么如何才能创建层叠上下文呢?我在网上看到过一个, 总结的很好: 目前有三类方法创建层叠上下文



    • 元素自身就能创建的

    • 需要结合z-index才能创建的

    • 不需要z-index 就能创建的


    一、元素自身形成层叠上下文


    文档根元素(<html>)会自动形成一个层叠上下文, 不需要结合任何其他属性


    二、需要配合z-index才能触发创建层叠上下文的


    position值为 absolute(绝对定位)或 relative(相对定位)且 z-index 属性值不为 auto 的元素;


    采用flex布局容器的子元素, 且子元素 z-index 属性值不为 auto 的元素;


    三、不要配合z-index就能触发创建层叠上下文的


    position值为 fixed(固定定位)或 sticky(粘滞定位)的元素;


    透明度opacity属性值不为1的元素


    转换transform属性值不为none的元素


    滤镜filter属性值不为none的元素


    上面列举出来的都是一些常用到的属性,当然还有其他的属性值设置也能触发形成层叠上下文,这里就不一一列举了,有兴趣的同学可以去MDN文档查看.这里我们需要注意的几点:



    • 层叠上下文可以包含在其他层叠上下文中, 由于根元素HTML本身就是一个层叠上下文,所以页面文档中的创建的层叠上下文都是HTML元素层级的一个子级

    • 当某个元素创建了层叠上下文后, 应当把它及其后代当成一个整体,去判断层叠顺序

    • 父子元素、兄弟元素都可能会处于同一层叠上下文中


    层叠级别

    在不考虑层叠上下文的情况下, 元素的层叠级别就是判断发生层叠时,元素在Z轴如何显示的依据, 下图就是著名的7阶层叠水平:


    background/border、负z-index元素、块级元素、浮动元素、行内/行内块元素、z-index为0元素、正z-index元素.

    文本节点我们也看成是一个行内元素

    判断层叠顺序

    在理解了层叠上下文和元素层叠水平的概念后,现在我们就可以说说元素在Z轴上的层叠顺序到底是怎么回事了: 元素在Z轴上的层叠顺序取决于两个方面: 元素所在的层叠上下文、元素自身的层叠级别,如果抛开层叠上下文来判断元素在Z轴上的层叠顺序就是瞎胡闹


    1. 当要比较的两个元素在同一层叠上下文时, 就按照元素自身的层叠级别, 如果级别相同时后则覆盖前者




    • 同一层叠上下文中的兄弟元素



    上图box1、box2都处在同一层叠上下文中(html元素形成的上下文) ,二者都是行内块元素,级别相同, 所以后者覆盖前者

    • 上图box1、box2也都处在同一层叠上下文中(html元素形成的上下文),但box1是行内元素,box2是块级元素, 根据元素的层叠级别,行内元素要高于块级元素, 所以box1显示在box2上面; 但是有个奇怪的现象, box1只能覆盖box2的背景,却不能覆盖box2内的字体.......为啥呢? 其实这个现象我们在上面也有提到过: 文本节点我们也看成是一个行内元素, 由于行内元素的级别要高于background/border,所以box1不能覆盖box2元素内的文本节点.




    • 同一层叠上下文中的父子元素


      也有可能是父子元素会出现在同一层叠上下文中, 其实刚刚上面我们说的字体的例子,就可以看成是父子元素在同一层叠上下文中, 这里就不在另外举例啦




    2. 当要比较的两个元素不在同一层叠上下文时, 需要先向上查找到两者所在的共同的且最近的层叠上下文,然后在根据第1条规则来判断




    链接:https://juejin.cn/post/6967737753983254564
    收起阅读 »

    iOS WKWebView实现JS与Objective-C交互(一) 附Demo

    前言: 根据需求有时候需要用到JS与Objective-C交互来实现一些功能, 本文介绍实现交互的一种方式, 使用WKWebView的新特性MessageHandler, 来实现JS调用原生, 原生调用JS.一. 基础说明WKWebView 初始化时,有一个参...
    继续阅读 »

    前言: 根据需求有时候需要用到JS与Objective-C交互来实现一些功能, 本文介绍实现交互的一种方式, 使用WKWebView的新特性MessageHandler, 来实现JS调用原生, 原生调用JS.

    一. 基础说明

    WKWebView 初始化时,有一个参数叫configuration,它是WKWebViewConfiguration类型的参数,而WKWebViewConfiguration有一个属性叫userContentController,它又是WKUserContentController类型的参数。WKUserContentController对象有一个方法- addScriptMessageHandler:name:,我把这个功能简称为MessageHandler。- addScriptMessageHandler:name:有两个参数,第一个参数是userContentController的代理对象,第二个参数是JS里发送postMessage的对象。
    所以要使用MessageHandler功能,就必须要实现WKScriptMessageHandler协议。

    二. 在JS中使用方法


    1. js文件代码实例

    function locationClick() {
    /// "showMessage". 为我们和前端开发人员的约定
    window.webkit.messageHandlers.showMessage.postMessage(null);
    }

    2. 在ViewController 我们需要做哪些事情

    2.1 对WKWebView进行初始化以及设置

    /// 创建网页配置对象
    WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
    /// 创建设置对象
    WKPreferences *preference = [[WKPreferences alloc]init];
    /// 最小字体大小 当将javaScriptEnabled属性设置为NO时,可以看到明显的效果
    preference.minimumFontSize = 40.0;
    /// 设置是否支持javaScript 默认是支持的
    preference.javaScriptEnabled = YES;
    /// 在iOS上默认为NO,表示是否允许不经过用户交互由javaScript自动打开窗口
    preference.javaScriptCanOpenWindowsAutomatically = YES;
    config.preferences = preference;
    /// 这个类主要用来做native与JavaScript的交互管理

    _wkwebView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height) configuration:config];
    [self.view addSubview:_wkwebView];
    /// Load WebView
    #if 0
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://m.benlai.com/huanan/zt/1231cherry"]];
    [self.wkwebView loadRequest:request];
    #endif

    #if 1
    NSString *bundleStr = [[NSBundle mainBundle] pathForResource:@"summerxx-test" ofType:@"html"];
    [self.wkwebView loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:bundleStr]]];
    #endif

    // UI代理
    _wkwebView.UIDelegate = self;
    // 导航代理
    _wkwebView.navigationDelegate = self;
    // 是否允许手势左滑返回上一级, 类似导航控制的左滑返回
    _wkwebView.allowsBackForwardNavigationGestures = YES;

    // 添加监测网页加载进度的观察者
    [self.wkwebView addObserver:self
    forKeyPath:@"estimatedProgress"
    options:0
    context:nil];
    // 添加监测网页标题title的观察者
    [self.wkwebView addObserver:self
    forKeyPath:@"title"
    options:NSKeyValueObservingOptionNew
    context:nil];

    2.2 在合理地方进行注册

    [self.wkwebView.configuration.userContentController addScriptMessageHandler:self name:@"showMessage"];

    2.3 接收JS给我们传递消息, 这里我做了一个简单的弹窗提示

    #pragma mark - WKScriptMessageHandler
    /// 通过接收JS传出消息的name进行捕捉的回调方法
    - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
    {
    if ([message.name isEqualToString:@"showMessage"]) {
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"title" message:@"messgae" preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"同意" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {

    NSString *jsStr = [NSString stringWithFormat:@"setLocation('%@')",@"虽然我同意了你, 但是答应我别骄傲."];
    [self.wkwebView evaluateJavaScript:jsStr completionHandler:^(id _Nullable result, NSError * _Nullable error) {
    NSLog(@"%@----%@",result, error);
    }];
    }];
    UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
    NSLog(@"cancel");

    }];
    UIAlertAction *errorAction = [UIAlertAction actionWithTitle:@"拒绝" style:UIAlertActionStyleDestructive handler:^(UIAlertAction * _Nonnull action) {
    NSString *jsStr = [NSString stringWithFormat:@"setLocation('%@')",@"虽然我拒绝了你, 但是继续爱我好吗"];
    [self.wkwebView evaluateJavaScript:jsStr completionHandler:^(id _Nullable result, NSError * _Nullable error) {
    NSLog(@"%@----%@",result, error);
    }];
    }];
    [alertController addAction:errorAction];
    [alertController addAction:okAction];
    [alertController addAction:cancelAction];

    // 出现
    [self presentViewController:alertController animated:YES completion:^{

    }];

    }
    }

    2.4 销毁

    - (void)dealloc {
    /// Remove removeObserver
    [_wkwebView removeObserver:self
    forKeyPath:NSStringFromSelector(@selector(estimatedProgress))];
    [_wkwebView removeObserver:self
    forKeyPath:NSStringFromSelector(@selector(title))];
    WKUserContentController *userCC = self.wkwebView.configuration.userContentController;
    [userCC removeScriptMessageHandlerForName:@"showMessage"];
    }

    Demo: 演示步骤, 点击获取定位 Objective-C获取到JS消息
    点击拒绝, JS获取到Objective-C传递的消息

    如图:



    总结: 脑壳疼
    备注: 如果遇到跨域问题, 主要还是前端和服务端改一下就好了.

    参照 : https://www.jianshu.com/p/433e59c5a9eb

    demo: https://github.com/summerxx27/JS_ObjectiveC_MessageHandler

    完~
    文/夏天然后

    转自:https://www.jianshu.com/p/d798786e99eb

    收起阅读 »

    整洁的 Table View 代码

    Table view 是 iOS 应用程序中非常通用的组件。许多代码和 table view 都有直接或间接的关系,随便举几个例子,比如提供数据、更新 table view,控制它的行为以及响应选择事件。在这篇文章中,我们将会展示保持 table view 相...
    继续阅读 »

    Table view 是 iOS 应用程序中非常通用的组件。许多代码和 table view 都有直接或间接的关系,随便举几个例子,比如提供数据、更新 table view,控制它的行为以及响应选择事件。在这篇文章中,我们将会展示保持 table view 相关代码的整洁和良好组织的技术。

    UITableViewController vs. UIViewController

    Apple 提供了 UITableViewController 作为 table views 专属的 view controller 类。Table view controllers 实现了一些非常有用的特性,来帮你避免一遍又一遍地写那些死板的代码!但是话又说回来,table view controller 只限于管理一个全屏展示的 table view。大多数情况下,这就是你想要的,但如果不是,还有其他方法来解决这个问题,就像下面我们展示的那样。

    Table View Controllers 的特性

    Table view controllers 会在第一次显示 table view 的时候帮你加载其数据。另外,它还会帮你切换 table view 的编辑模式、响应键盘通知、以及一些小任务,比如闪现侧边的滑动提示条和清除选中时的背景色。为了让这些特性生效,当你在子类中覆写类似 viewWillAppear: 或者 viewDidAppear: 等事件方法时,需要调用 super 版本。

    Table view controllers 相对于标准 view controllers 的一个特别的好处是它支持 Apple 实现的“下拉刷新”。目前,文档中唯一的使用 UIRefreshControl 的方式就是通过 table view controller ,虽然通过努力在其他地方也能让它工作(见此处),但很可能在下一次 iOS 更新的时候就不行了。

    这些要素加一起,为我们提供了大部分 Apple 所定义的标准 table view 交互行为,如果你的应用恰好符合这些标准,那么直接使用 table view controllers 来避免写那些死板的代码是个很好的方法。

    Table View Controllers 的限制

    Table view controllers 的 view 属性永远都是一个 table view。如果你稍后决定在 table view 旁边显示一些东西(比如一个地图),如果不依赖于那些奇怪的 hacks,估计就没什么办法了。

    如果你是用代码或 .xib 文件来定义的界面,那么迁移到一个标准 view controller 将会非常简单。但是如果你使用了 storyboards,那么这个过程要多包含几个步骤。除非重新创建,否则你并不能在 storyboards 中将 table view controller 改成一个标准的 view controller。这意味着你必须将所有内容拷贝到新的 view controller,然后再重新连接一遍。

    最后,你需要把迁移后丢失的 table view controller 的特性给补回来。大多数都是 viewWillAppear: 或 viewDidAppear: 中简单的一条语句。切换编辑模式需要实现一个 action 方法,用来切换 table view 的 editing 属性。大多数工作来自重新创建对键盘的支持。

    在选择这条路之前,其实还有一个更轻松的选择,它可以通过分离我们需要关心的功能(关注点分离),让你获得额外的好处:

    使用 Child View Controllers

    和完全抛弃 table view controller 不同,你还可以将它作为 child view controller 添加到其他 view controller 中(关于此话题的文章)。这样,parent view controller 在管理其他的你需要的新加的界面元素的同时,table view controller 还可以继续管理它的 table view。

    - (void)addPhotoDetailsTableView
    {
    DetailsViewController *details = [[DetailsViewController alloc] init];
    details.photo = self.photo;
    details.delegate = self;
    [self addChildViewController:details];
    CGRect frame = self.view.bounds;
    frame.origin.y = 110;
    details.view.frame = frame;
    [self.view addSubview:details.view];
    [details didMoveToParentViewController:self];
    }

    如果你使用这个解决方案,你就必须在 child view controller 和 parent view controller 之间建立消息传递的渠道。比如,如果用户选择了一个 table view 中的 cell,parent view controller 需要知道这个事件来推入其他 view controller。根据使用习惯,通常最清晰的方式是为这个 table view controller 定义一个 delegate protocol,然后到 parent view controller 中去实现。

    @protocol DetailsViewControllerDelegate
    - (void)didSelectPhotoAttributeWithKey:(NSString *)key;
    @end

    @interface PhotoViewController ()
    @end

    @implementation PhotoViewController
    // ...
    - (void)didSelectPhotoAttributeWithKey:(NSString *)key
    {
    DetailViewController *controller = [[DetailViewController alloc] init];
    controller.key = key;
    [self.navigationController pushViewController:controller animated:YES];
    }
    @end

    就像你看到的那样,这种结构为 view controller 之间的消息传递带来了额外的开销,但是作为回报,代码封装和分离非常清晰,有更好的复用性。根据实际情况的不同,这既可能让事情变得更简单,也可能会更复杂,需要读者自行斟酌和决定。

    分离关注点(Separating Concerns)

    当处理 table views 的时候,有许多各种各样的任务,这些任务穿梭于 models,controllers 和 views 之间。为了避免让 view controllers 做所有的事,我们将尽可能地把这些任务划分到合适的地方,这样有利于阅读、维护和测试。

    这里描述的技术是文章更轻量的 View Controllers 中的概念的延伸,请参考这篇文章来理解如何重构 data source 和 model 的逻辑。结合 table views,我们来具体看看如何在 view controllers 和 views 之间分离关注点。

    搭建 Model 对象和 Cells 之间的桥梁

    有时我们需要将想显示的 model 层中的数据传到 view 层中去显示。由于我们同时也希望让 model 和 view 之间明确分离,所以通常把这个任务转移到 table view 的 data source 中去处理:

    - (UITableViewCell *)tableView:(UITableView *)tableView
    cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
    PhotoCell *cell = [tableView dequeueReusableCellWithIdentifier:@"PhotoCell"];
    Photo *photo = [self itemAtIndexPath:indexPath];
    cell.photoTitleLabel.text = photo.name;
    NSString* date = [self.dateFormatter stringFromDate:photo.creationDate];
    cell.photoDateLabel.text = date;
    }

    但是这样的代码会让 data source 变得混乱,因为它向 data source 暴露了 cell 的设计。最好分解出来,放到 cell 类的一个 category 中。

    @implementation PhotoCell (ConfigureForPhoto)

    - (void)configureForPhoto:(Photo *)photo
    {
    self.photoTitleLabel.text = photo.name;
    NSString* date = [self.dateFormatter stringFromDate:photo.creationDate];
    self.photoDateLabel.text = date;
    }

    @end

    有了上述代码后,我们的 data source 方法就变得简单了。

    - (UITableViewCell *)tableView:(UITableView *)tableView
    cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
    PhotoCell *cell = [tableView dequeueReusableCellWithIdentifier:PhotoCellIdentifier];
    [cell configureForPhoto:[self itemAtIndexPath:indexPath]];
    return cell;
    }

    在我们的示例代码中,table view 的 data source 已经分解到单独的类中了,它用一个设置 cell 的 block 来初始化。这时,这个 block 就变得这样简单了:

    TableViewCellConfigureBlock block = ^(PhotoCell *cell, Photo *photo) {
    [cell configureForPhoto:photo];
    };

    让 Cells 可复用

    有时多种 model 对象需要用同一类型的 cell 来表示,这种情况下,我们可以进一步让 cell 可以复用。首先,我们给 cell 定义一个 protocol,需要用这个 cell 显示的对象必须遵循这个 protocol。然后简单修改 category 中的设置方法,让它可以接受遵循这个 protocol 的任何对象。这些简单的步骤让 cell 和任何特殊的 model 对象之间得以解耦,让它可适应不同的数据类型。

    在 Cell 内部控制 Cell 的状态

    如果你想自定义 table views 默认的高亮或选择行为,你可以实现两个 delegate 方法,把点击的 cell 修改成我们想要的样子。例如:

    - (void)tableView:(UITableView *)tableView
    didHighlightRowAtIndexPath:(NSIndexPath *)indexPath
    {
    PhotoCell *cell = [tableView cellForRowAtIndexPath:indexPath];
    cell.photoTitleLabel.shadowColor = [UIColor darkGrayColor];
    cell.photoTitleLabel.shadowOffset = CGSizeMake(3, 3);
    }

    - (void)tableView:(UITableView *)tableView
    didUnhighlightRowAtIndexPath:(NSIndexPath *)indexPath
    {
    PhotoCell *cell = [tableView cellForRowAtIndexPath:indexPath];
    cell.photoTitleLabel.shadowColor = nil;
    }

    然而,这两个 delegate 方法的实现又基于了 view controller 知晓 cell 实现的具体细节。如果我们想替换或重新设计 cell,我们必须改写 delegate 代码。View 的实现细节和 delegate 的实现交织在一起了。我们应该把这些细节移到 cell 自身中去。

    @implementation PhotoCell
    // ...
    - (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated
    {
    [super setHighlighted:highlighted animated:animated];
    if (highlighted) {
    self.photoTitleLabel.shadowColor = [UIColor darkGrayColor];
    self.photoTitleLabel.shadowOffset = CGSizeMake(3, 3);
    } else {
    self.photoTitleLabel.shadowColor = nil;
    }
    }
    @end

    总的来说,我们在努力把 view 层和 controller 层的实现细节分离开。delegate 肯定得清楚一个 view 该显示什么状态,但是它不应该了解如何修改 view 结构或者给某些 subviews 设置某些属性以获得正确的状态。所有这些逻辑都应该封装到 view 内部,然后给外部提供一个简单的 API。

    控制多个 Cell 类型

    如果一个 table view 里面有多种类型的 cell,data source 方法很快就难以控制了。在我们示例程序中,photo details table 有两种不同类型的 cell:一种用于显示几个星,另一种用来显示一个键值对。为了划分处理不同 cell 类型的代码,data source 方法简单地通过判断 cell 的类型,把任务派发给其他指定的方法。

    - (UITableViewCell *)tableView:(UITableView *)tableView
    cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
    NSString *key = self.keys[(NSUInteger) indexPath.row];
    id value = [self.photo valueForKey:key];
    UITableViewCell *cell;
    if ([key isEqual:PhotoRatingKey]) {
    cell = [self cellForRating:value indexPath:indexPath];
    } else {
    cell = [self detailCellForKey:key value:value];
    }
    return cell;
    }

    - (RatingCell *)cellForRating:(NSNumber *)rating
    indexPath:(NSIndexPath *)indexPath
    {
    // ...
    }

    - (UITableViewCell *)detailCellForKey:(NSString *)key
    value:(id)value
    {
    // ...
    }

    编辑 Table View

    Table view 提供了易于使用的编辑特性,允许你对 cell 进行删除或重新排序。这些事件都可以让 table view 的 data source 通过 delegate 方法得到通知。因此,通常我们能在这些 delegate 方法中看到对数据的进行修改的操作。

    修改数据很明显是属于 model 层的任务。Model 应该为诸如删除或重新排序等操作暴露一个 API,然后我们可以在 data source 方法中调用它。这样,controller 就可以扮演 view 和 model 之间的协调者,而不需要知道 model 层的实现细节。并且还有额外的好处,model 的逻辑也变得更容易测试,因为它不再和 view controllers 的任务混杂在一起了。

    总结

    Table view controllers(以及其他的 controller 对象!)应该在 model 和 view 对象之间扮演协调者和调解者的角色。它不应该关心明显属于 view 层或 model 层的任务。你应该始终记住这点,这样 delegate 和 data source 方法会变得更小巧,最多包含一些简单的样板代码。

    这不仅减少了 table view controllers 那样的大小和复杂性,而且还把业务逻辑和 view 的逻辑放到了更合适的地方。Controller 层的里里外外的实现细节都被封装成了简单的 API,最终,它变得更加容易理解,也更利于团队协作。

    转自:https://www.jianshu.com/p/22df7a214b49

    收起阅读 »

    Core Image 和视频

    在这篇文章中,我们将研究如何将 Core Image 应用到实时视频上去。我们会看两个例子:首先,我们把这个效果加到相机拍摄的影片上去。之后,我们会将这个影响作用于拍摄好的视频文件。它也可以做到离线渲染,它会把渲染结果返回给视频,而不是直接显示在屏幕上。两个例...
    继续阅读 »

    在这篇文章中,我们将研究如何将 Core Image 应用到实时视频上去。我们会看两个例子:首先,我们把这个效果加到相机拍摄的影片上去。之后,我们会将这个影响作用于拍摄好的视频文件。它也可以做到离线渲染,它会把渲染结果返回给视频,而不是直接显示在屏幕上。两个例子的完整源代码,请点击这里

    总览

    当涉及到处理视频的时候,性能就会变得非常重要。而且了解黑箱下的原理 —— 也就是 Core Image 是如何工作的 —— 也很重要,这样我们才能达到足够的性能。在 GPU 上面做尽可能多的工作,并且最大限度的减少 GPU 和 CPU 之间的数据传送是非常重要的。之后的例子中,我们将看看这个细节。

    想对 Core Image 有个初步认识的话,可以读读 Warren 的这篇文章 Core Image 介绍。我们将使用 Swift 的函数式 API 中介绍的基于 CIFilter 的 API 封装。想要了解更多关于 AVFoundation 的知识,可以看看本期话题中 Adriaan 的文章,还有话题 #21 中的 iOS 上的相机捕捉。

    优化资源的 OpenGL ES

    CPU 和 GPU 都可以运行 Core Image,我们将会在 下面 详细介绍这两个的细节。在这个例子中,我们要使用 GPU,我们做如下几样事情。

    我们首先创建一个自定义的 UIView,它允许我们把 Core Image 的结果直接渲染成 OpenGL。我们可以新建一个 GLKView 并且用一个 EAGLContext 来初始化它。我们需要指定 OpenGL ES 2 作为渲染 API,在这两个例子中,我们要自己触发 drawing 事件 (而不是在 -drawRect: 中触发),所以在初始化 GLKView 的时候,我们将 enableSetNeedsDisplay 设置为 false。之后我们有可用新图像的时候,我们需要主动去调用 -display。

    在这个视图里,我们保持一个对 CIContext 的引用,它提供一个桥梁来连接我们的 Core Image 对象和 OpenGL 上下文。我们创建一次就可以一直使用它。这个上下文允许 Core Image 在后台做优化,比如缓存和重用纹理之类的资源等。重要的是这个上下文我们一直在重复使用。

    上下文中有一个方法,-drawImage:inRect:fromRect:,作用是绘制出来一个 CIImage。如果你想画出来一个完整的图像,最容易的方法是使用图像的 extent。但是请注意,这可能是无限大的,所以一定要事先裁剪或者提供有限大小的矩形。一个警告:因为我们处理的是 Core Image,绘制的目标以像素为单位,而不是点。由于大部分新的 iOS 设备配备 Retina 屏幕,我们在绘制的时候需要考虑这一点。如果我们想填充整个视图,最简单的办法是获取视图边界,并且按照屏幕的 scale 来缩放图片 (Retina 屏幕的 scale 是 2)。

    完整的代码示例在这里:CoreImageView.swift

    从相机获取像素数据

    对于 AVFoundation 如何工作的概述,请看 Adriaan 的文章 和 Matteo 的文章 iOS 上的相机捕捉。对于我们而言,我们想从镜头获得 raw 格式的数据。我们可以通过创建一个 AVCaptureDeviceInput 对象来选定一个摄像头。使用 AVCaptureSession,我们可以把它连接到一个 AVCaptureVideoDataOutput。这个 data output 对象有一个遵守 AVCaptureVideoDataOutputSampleBufferDelegate 协议的代理对象。这个代理每一帧将接收到一个消息:

    func captureOutput(captureOutput: AVCaptureOutput!,
    didOutputSampleBuffer: CMSampleBuffer!,
    fromConnection: AVCaptureConnection!) {

    我们将用它来驱动我们的图像渲染。在我们的示例代码中,我们已经将配置,初始化以及代理对象都打包到了一个叫做 CaptureBufferSource 的简单接口中去。我们可以使用前置或者后置摄像头以及一个回调来初始化它。对于每个样本缓存区,这个回调都会被调用,并且参数是缓冲区和对应摄像头的 transform:

    source = CaptureBufferSource(position: AVCaptureDevicePosition.Front) {
    (buffer, transform) in
    ...
    }

    我们需要对相机返回的数据进行变换。无论你如何转动 iPhone,相机的像素数据的方向总是相同的。在我们的例子中,我们将 UI 锁定在竖直方向,我们希望屏幕上显示的图像符合照相机拍摄时的方向,为此我们需要后置摄像头拍摄出的图片旋转 -π/2。前置摄像头需要旋转 -π/2 并且加一个镜像效果。我们可以用一个 CGAffineTransform 来表达这种变换。请注意如果 UI 是不同的方向 (比如横屏),我们的变换也将是不同的。还要注意,这种变换的代价其实是非常小的,因为它是在 Core Image渲染管线中完成的。

    接着,要把 CMSampleBuffer转换成 CIImage,我们首先需要将它转换成一个 CVPixelBuffer。我们可以写一个方便的初始化方法来为我们做这件事:

    extension CIImage {
    convenience init(buffer: CMSampleBuffer) {
    self.init(CVPixelBuffer: CMSampleBufferGetImageBuffer(buffer))
    }
    }

    现在我们可以用三个步骤来处理我们的图像。首先,把我们的 CMSampleBuffer 转换成 CIImage,并且应用一个形变,使图像旋转到正确的方向。接下来,我们用一个 CIFilter 滤镜来得到一个新的 CIImage 输出。我们使用了 Florian 的文章 提到的创建滤镜的方式。在这个例子中,我们使用色调调整滤镜,并且传入一个依赖于时间而变化的调整角度。最终,我们使用之前定义的 View,通过 CIContext 来渲染 CIImage。这个流程非常简单,看起来是这样的:

    source = CaptureBufferSource(position: AVCaptureDevicePosition.Front) {
    [unowned self] (buffer, transform) in
    let input = CIImage(buffer: buffer).imageByApplyingTransform(transform)
    let filter = hueAdjust(self.angleForCurrentTime)
    self.coreImageView?.image = filter(input)
    }

    当你运行它时,你可能会因为如此低的 CPU 使用率感到吃惊。这其中的奥秘是 GPU 做了几乎所有的工作。尽管我们创建了一个 CIImage,应用了一个滤镜,并输出一个 CIImage,最终输出的结果是一个 promise:直到实际渲染才会去进行计算。一个 CIImage 对象可以是黑箱里的很多东西,它可以是 GPU 算出来的像素数据,也可以是如何创建像素数据的一个说明 (比如使用一个滤镜生成器),或者它也可以是直接从 OpenGL 纹理中创建出来的图像。

    从影片中获取像素数据

    我们可以做的另一件事是通过 Core Image 把这个滤镜加到一个视频中。和实时拍摄不同,我们现在从影片的每一帧中生成像素缓冲区,在这里我们将采用略有不同的方法。对于相机,它会推送每一帧给我们,但是对于已有的影片,我们使用拉取的方式:通过 display link,我们可以向 AVFoundation 请求在某个特定时间的一帧。

    display link 对象负责在每帧需要绘制的时候给我们发送消息,这个消息是按照显示器的刷新频率同步进行发送的。这通常用来做 自定义动画,但也可以用来播放和操作视频。我们要做的第一件事就是创建一个 AVPlayer和一个视频输出:

    player = AVPlayer(URL: url)
    videoOutput = AVPlayerItemVideoOutput(pixelBufferAttributes: pixelBufferDict)
    player.currentItem.addOutput(videoOutput)

    接下来,我们要创建 display link。方法很简单,只要创建一个 CADisplayLink 对象,并将其添加到 run loop。

    let displayLink = CADisplayLink(target: self, selector: "displayLinkDidRefresh:")
    displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)

    现在,唯一剩下的就是在 displayLinkDidRefresh: 调用的时候获取视频每一帧。首先,我们获取当前的时间,并且将它转换成当前播放项目里的时间比。然后我们询问 videoOutput,如果当前时间有一个可用的新的像素缓存区,我们把它复制一下并且调用回调方法:

    func displayLinkDidRefresh(link: CADisplayLink) {
    let itemTime = videoOutput.itemTimeForHostTime(CACurrentMediaTime())
    if videoOutput.hasNewPixelBufferForItemTime(itemTime) {
    let pixelBuffer = videoOutput.copyPixelBufferForItemTime(itemTime, itemTimeForDisplay: nil)
    consumer(pixelBuffer)
    }
    }

    我们从一个视频输出获得的像素缓冲是一个 CVPixelBuffer,我们可以把它直接转换成 CIImage。正如上面的例子,我们会加上一个滤镜。在这个例子里,我们将组合多个滤镜:我们使用一个万花筒的效果,然后用渐变遮罩把原始图像和过滤图像相结合,这个操作是非常轻量级的。

    创意地使用滤镜

    大家都知道流行的照片效果。虽然我们可以将这些应用到视频,但 Core Image 还可以做得更多。

    Core Image 里所谓的滤镜有不同的类别。其中一些是传统的类型,输入一张图片并且输出一张新的图片。但有些需要两个 (或者更多) 的输入图像并且混合生成一张新的图像。另外甚至有完全不输入图片,而是基于参数的生成图像的滤镜。

    通过混合这些不同的类型,我们可以创建意想不到的效果。

    混合图片

    在这个例子中,我们使用这些东西:


    上面的例子可以将图像的一个圆形区域像素化。

    它也可以创建交互,我们可以使用触摸事件来改变所产生的圆的位置。

    Core Image Filter Reference 按类别列出了所有可用的滤镜。请注意,有一部分只能用在 OS X。

    生成器和渐变滤镜可以不需要输入就能生成图像。它们很少自己单独使用,但是作为蒙版的时候会非常强大,就像我们例子中的 CIBlendWithMask 那样。

    混合操作和 CIBlendWithAlphaMask 还有 CIBlendWithMask 允许将两个图像合并成一个。

    CPU vs. GPU

    我们在话题 #3 的文章,绘制像素到屏幕上里,介绍了 iOS 和 OS X 的图形栈。需要注意的是 CPU 和 GPU 的概念,以及两者之间数据的移动方式。

    在处理实时视频的时候,我们面临着性能的挑战。

    首先,我们需要能在每一帧的时间内处理完所有的图像数据。我们的样本中采用 24 帧每秒的视频,这意味着我们有 41 毫秒 (1/24 秒) 的时间来解码,处理以及渲染每一帧中的百万像素。

    其次,我们需要能够从 CPU 或者 GPU 上面得到这些数据。我们从视频文件读取的字节数最终会到达 CPU 里。但是这个数据还需要移动到 GPU 上,以便在显示器上可见。

    避免转移

    一个非常致命的问题是,在渲染管线中,代码可能会把图像数据在 CPU 和 GPU 之间来回移动好几次。确保像素数据仅在一个方向移动是很重要的,应该保证数据只从 CPU 移动到 GPU,如果能让数据完全只在 GPU 上那就更好。

    如果我们想渲染 24 fps 的视频,我们有 41 毫秒;如果我们渲染 60 fps 的视频,我们只有 16 毫秒,如果我们不小心从 GPU 下载了一个像素缓冲到 CPU 里,然后再上传回 GPU,对于一张全屏的 iPhone 6 图像来说,我们在每个方向将要移动 3.8 MB 的数据,这将使帧率无法达标。

    当我们使用 CVPixelBuffer 时,我们希望这样的流程:


    CVPixelBuffer 是基于 CPU 的 (见下文),我们用 CIImage 来包装它。构建滤镜链不会移动任何数据;它只是建立了一个流程。一旦我们绘制图像,我们使用了基于 EAGL 上下文的 Core Image 上下文,而这个 EAGL 上下文也是 GLKView 进行图像显示所使用的上下文。EAGL 上下文是基于 GPU 的。请注意,我们是如何只穿越 GPU-CPU 边界一次的,这是至关重要的部分。

    工作和目标

    Core Image 的图形上下文可以通过两种方式创建:使用 EAGLContext 的 GPU 上下文,或者是基于 CPU 的上下文。

    这个定义了 Core Image 工作的地方,也就是像素数据将被处理的地方。与工作区域无关,基于 GPU 和基于 CPU 的图形上下文都可以通过执行 createCGImage(…),render(, toBitmap, …) 和 render(, toCVPixelBuffer, …),以及相关的命令来向 CPU 进行渲染。

    重要的是要理解如何在 CPU 和 GPU 之间移动像素数据,或者是让数据保持在 CPU 或者 GPU 里。将数据移过这个边界是需要很大的代价的。

    缓冲区和图像

    在我们的例子中,我们使用了几个不同的缓冲区和图像。这可能有点混乱。这样做的原因很简单,不同的框架对于这些“图像”有不同的用途。下面有一个快速总览,以显示哪些是以基于 CPU 或者基于 GPU 的:


    结论

    Core Image 是操纵实时视频的一大利器。只要你适当的配置下,性能将会是强劲的 —— 只要确保 CPU 和 GPU 之间没有数据的转移。创意地使用滤镜,你可以实现一些非常炫酷的效果,神马简单色调,褐色滤镜都弱爆啦。所有的这些代码都很容易抽象出来,深入了解下不同的对象的作用区域 (GPU 还是 CPU) 可以帮助你提高代码的性能。

    转自:https://www.jianshu.com/p/6c4118290a56

    收起阅读 »

    基于环信 sdk 在uni-app框架中快速集成开发的一款多平台社交Demo

    说在前面:此款 demo 是基于 环信sdk 开发的一款具有单聊、群聊、聊天室、音视频等功能的应用。在此之前我们已经开发完 Vue、react(web端)、微信小程序。这三个热门领域的版本,如有需要源码可以留言给我 Git 源码地址: https:/...
    继续阅读 »

    说在前面:此款 demo 是基于 环信sdk 开发的一款具有单聊、群聊、聊天室、音视频等功能的应用。在此之前我们已经开发完 Vue、react(web端)、微信小程序。这三个热门领域的版本,如有需要源码可以留言给我


    Git 源码地址: https://github.com/easemob/webim-uniapp-demo


    一、安装开发工具


    我们选用微信小程序来用做示例(如果选择百度、支付宝安装对应开发者工具即可)、


    微信开发者工具建议还是安装最新版的。uni-app的开发也必须安装HBuilderX工具,这个是捆绑的,没得选择。要用uni-app,你必须得装!


    工具安装:


    微信开发者工具


    HBuilderX


    项目demo介绍:



    项目demo启动预览:



    快速集成环信 sdk:


    1、复制整个utils文件



    如果你想具体了解主要配置文件 请看这个链接:


    https://docs-im.easemob.com/im/web/intro/start


    2、如何使用环信的appkey ,可以在环信 console 后台注册一个 账号申请appkey ,可以参考这里 ,获取到 appkey 以后添加到配置文件中 ,如下图所示:



    以上两个重要的配置准备完成之后就可以进行一系列的操作了(收发消息、好友申请、进群入群通知等)


    在uni-app中 使用环信 sdk 实现添加、删除好友:


    1、在全局 App.vue 文件 钩子函数 onLaunch() 中监听各种事件 (好友申请、收到各类消息等)如图:



    发送好友请求:



    onPresence(message)事件中接收到好友消息申请:



    同意好友请求:



    拒绝好友请求:



    实现收发消息:


    1、给好友发送消息:



    2、接收到消息:


    onTextMessage(message)事件中接收到好友消息,然后做消息上屏处理(具体消息上屏逻辑可看demo中代码示例):



    以上展示的仅仅为基本业务场景,更多的业务逻辑详情请看demo示例。api具体详情可以查看 环信sdk 文档


                                      


    PS:对于安卓、iOS移动端,我们已经兼容完成。想通过uni-app生成安卓、ios应用的小伙伴们可以愉快的使用起来了~~~


    基于uni-app的开发其中也趟了不少坑,在这里就不多赘述了。回归到框架的选型来讲,选用uni-app开发小程序,可同时并行多端小程序,这点是真香,一次开发多端发布。至于审核嘛~ 时快时慢


    最后的最后:如果你喜欢,请拒绝白嫖,点赞三连转发!


                                                   

    收起阅读 »

    GCD你会用吗?GCD扫盲之dispatch_semaphore

    本文是GCD多线程编程中dispatch_semaphore内容的小结,通过本文,你可以了解到:1、信号量的基本概念与基本使用2、信号量在线程同步与资源加锁方面的应用3、信号量释放时的小陷阱今天我来讲解一下dispatch_semaphore在我们平常开发中的...
    继续阅读 »

    本文是GCD多线程编程中dispatch_semaphore内容的小结,通过本文,你可以了解到:

    1、信号量的基本概念与基本使用
    2、信号量在线程同步与资源加锁方面的应用
    3、信号量释放时的小陷阱

    今天我来讲解一下dispatch_semaphore在我们平常开发中的一些基本概念与基本使用,dispatch_semaphore俗称信号量,也称为信号锁,在多线程编程中主要用于控制多线程下访问资源的数量,比如系统有两个资源可以使用,但同时有三个线程要访问,所以只能允许两个线程访问,第三个应当等待资源被释放后再访问,这时我们就可以使用dispatch_semaphore。

    与dispatch_semaphore相关的共有3个方法,分别是dispatch_semaphore_create,dispatch_semaphore_wait,dispatch_semaphore_signal下面我们逐一了解一下这三个方法。

    测试代码在这

    semaphore的三个方法

    dispatch_semaphore_create

    /*!
    * @function dispatch_semaphore_create
    *
    * @abstract
    * Creates new counting semaphore with an initial value.
    *
    * @discussion
    * Passing zero for the value is useful for when two threads need to reconcile
    * the completion of a particular event. Passing a value greater than zero is
    * useful for managing a finite pool of resources, where the pool size is equal
    * to the value.
    *
    * @param value
    * The starting value for the semaphore. Passing a value less than zero will
    * cause NULL to be returned.
    *
    * @result
    * The newly created semaphore, or NULL on failure.
    */
    API_AVAILABLE(macos(10.6), ios(4.0))
    DISPATCH_EXPORT DISPATCH_MALLOC DISPATCH_RETURNS_RETAINED DISPATCH_WARN_RESULT
    DISPATCH_NOTHROW
    dispatch_semaphore_t
    dispatch_semaphore_create(long value);

    dispatch_semaphore_create方法用于创建一个带有初始值的信号量dispatch_semaphore_t。

    对于这个方法的参数信号量的初始值,这里有2种情况:

    1、信号量初始值为0时:这种情况主要用于两个线程需要协调特定事件的完成时,即线程同步。
    2、信号量初始值为大于0时:这种情况主要用于管理有限的资源池,其中池大小等于这个值,即资源加锁。
    上面的2种情况(线程同步、资源加锁),我们在后续的使用篇中会详细讲解。

    dispatch_semaphore_wait

    /*!
    * @function dispatch_semaphore_wait
    *
    * @abstract
    * Wait (decrement) for a semaphore.
    *
    * @discussion
    * Decrement the counting semaphore. If the resulting value is less than zero,
    * this function waits for a signal to occur before returning.
    *
    * @param dsema
    * The semaphore. The result of passing NULL in this parameter is undefined.
    *
    * @param timeout
    * When to timeout (see dispatch_time). As a convenience, there are the
    * DISPATCH_TIME_NOW and DISPATCH_TIME_FOREVER constants.
    *
    * @result
    * Returns zero on success, or non-zero if the timeout occurred.
    */
    API_AVAILABLE(macos(10.6), ios(4.0))
    DISPATCH_EXPORT DISPATCH_NONNULL_ALL DISPATCH_NOTHROW
    long
    dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);

    dispatch_semaphore_wait这个方法主要用于等待或减少信号量,每次调用这个方法,信号量的值都会减一,然后根据减一后的信号量的值的大小,来决定这个方法的使用情况,所以这个方法的使用同样也分为2种情况:

    1、当减一后的值小于0时,这个方法会一直等待,即阻塞当前线程,直到信号量+1或者直到超时。
    2、当减一后的值大于或等于0时,这个方法会直接返回,不会阻塞当前线程。
    上面2种方式,放到我们日常的开发中就是下面2种使用情况:

    · 当我们只需要同步线程时,我们可以使用dispatch_semaphore_create(0)初始化信号量为0,然后使用dispatch_semaphore_wait方法让信号量减一,这时就属于第一种减一后小于0的情况,这时就会阻塞当前线程,直到另一个线程调用dispatch_semaphore_signal这个让信号量加1的方法后,当前线程才会被唤醒,然后执行当前线程中的代码,这时就起到一个线程同步的作用。

    · 当我们需要对资源加锁,控制同时能访问资源的最大数量(假设为n)时,我们就需要使用dispatch_semaphore_create(n)方法来初始化信号量为n,然后使用dispatch_semaphore_wait方法将信号量减一,然后访问我们的资源,然后使用dispatch_semaphore_signal方法将信号量加一。如果有n个线程来访问这个资源,当这n个资源访问都还没有结束时,就会阻塞当前线程,第n+1个线程的访问就必须等待,直到前n个的某一个的资源访问结束,这就是我们很常见的资源加锁的情况。

    dispatch_semaphore_signal

    /*!
    * @function dispatch_semaphore_signal
    *
    * @abstract
    * Signal (increment) a semaphore.
    *
    * @discussion
    * Increment the counting semaphore. If the previous value was less than zero,
    * this function wakes a waiting thread before returning.
    *
    * @param dsema The counting semaphore.
    * The result of passing NULL in this parameter is undefined.
    *
    * @result
    * This function returns non-zero if a thread is woken. Otherwise, zero is
    * returned.
    */
    API_AVAILABLE(macos(10.6), ios(4.0))
    DISPATCH_EXPORT DISPATCH_NONNULL_ALL DISPATCH_NOTHROW
    long
    dispatch_semaphore_signal(dispatch_semaphore_t dsema);

    dispatch_semaphore_signal方法用于让信号量的值加一,然后直接返回。如果先前信号量的值小于0,那么这个方法还会唤醒先前等待的线程。

    semaphore使用篇

    线程同步

    这种情况在我们的开发中也是挺常见的,当主线程中有一个异步网络任务,我们需要等这个网络请求成功拿到数据后,才能继续做后面的处理,这时我们就可以使用信号量这种方式来进行线程同步。

    我们首先看看完整测试代码:

    - (IBAction)threadSyncTask:(UIButton *)sender {

    NSLog(@"threadSyncTask start --- thread:%@",[NSThread currentThread]);

    //1.创建一个初始值为0的信号量
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    //2.定制一个异步任务
    //开启一个异步网络请求
    NSLog(@"开启一个异步网络请求");
    NSURLSession *session = [NSURLSession sharedSession];
    NSURL *url =
    [NSURL URLWithString:[@"https://www.baidu.com/" stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    request.HTTPMethod = @"GET";

    NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    if (error) {
    NSLog(@"%@", [error localizedDescription]);
    }
    if (data) {
    NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
    NSLog(@"%@", dict);
    }
    NSLog(@"异步网络任务完成---%@",[NSThread currentThread]);
    //4.调用signal方法,让信号量+1,然后唤醒先前被阻塞的线程
    NSLog(@"调用dispatch_semaphore_signal方法");
    dispatch_semaphore_signal(semaphore);
    }];
    [dataTask resume];

    //3.调用wait方法让信号量-1,这时信号量小于0,这个方法会阻塞当前线程,直到信号量等于0时,唤醒当前线程
    NSLog(@"调用dispatch_semaphore_wait方法");
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    NSLog(@"threadSyncTask end --- thread:%@",[NSThread currentThread]);
    }

    运行之后的log如下:

    2019-04-27 17:24:27.050077+0800 GCD(四) dispatch_semaphore[34482:6102243] threadSyncTask end --- thread:<NSThread: 0x6000028aa7c0>{number = 1, name = main}
    2019-04-27 17:24:27.050227+0800 GCD(四) dispatch_semaphore[34482:6102243] 开启一个异步网络请求
    2019-04-27 17:24:27.050571+0800 GCD(四) dispatch_semaphore[34482:6102243] 调用dispatch_semaphore_wait方法
    2019-04-27 17:24:27.105069+0800 GCD(四) dispatch_semaphore[34482:6105851] (null)
    2019-04-27 17:24:27.105262+0800 GCD(四) dispatch_semaphore[34482:6105851] 异步网络任务完成---<NSThread: 0x6000028c6ec0>{number = 6, name = (null)}
    2019-04-27 17:24:27.105401+0800 GCD(四) dispatch_semaphore[34482:6105851] 调用dispatch_semaphore_signal方法
    2019-04-27 17:24:27.105550+0800 GCD(四) dispatch_semaphore[34482:6102243] threadSyncTask end --- thread:<NSThread: 0x6000028aa7c0>{number = 1, name = main}

    从log中我们可以看出,wait方法会阻塞主线程,直到异步任务完成调用signal方法,才会继续回到主线程执行后面的任务。

    资源加锁

    当一个资源可以被多个线程读取修改时,就会很容易出现多线程访问修改数据出现结果不一致甚至崩溃的问题。为了处理这个问题,我们通常使用的办法,就是使用NSLock,@synchronized给这个资源加锁,让它在同一时间只允许一个线程访问资源。其实信号量也可以当做一个锁来使用,而且比NSLock还有@synchronized代价更低一些,接下来我们来看看它的基本使用

    第一步,定义2个宏,将wait与signal方法包起来,方便下面的使用

    #ifndef ZED_LOCK
    #define ZED_LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
    #endif

    #ifndef ZED_UNLOCK
    #define ZED_UNLOCK(lock) dispatch_semaphore_signal(lock);
    #endif

    第二步,声明与创建共享资源与信号锁

    /* 需要加锁的资源 **/
    @property (nonatomic, strong) NSMutableDictionary *dict;

    /* 信号锁 **/
    @property (nonatomic, strong) dispatch_semaphore_t lock;
    //创建共享资源
    self.dict = [NSMutableDictionary dictionary];
    //初始化信号量,设置初始值为1
    self.lock = dispatch_semaphore_create(1);

    第三步,在即将使用共享资源的地方添加ZED_LOCK宏,进行信号量减一操作,在共享资源使用完成的时候添加ZED_UNLOCK,进行信号量加一操作。

    - (IBAction)resourceLockTask:(UIButton *)sender {

    NSLog(@"resourceLockTask start --- thread:%@",[NSThread currentThread]);

    //使用异步执行并发任务会开辟新的线程的特性,来模拟开辟多个线程访问贡献资源的场景

    for (int i = 0; i < 3; i++) {

    NSLog(@"异步添加任务:%d",i);

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    ZED_LOCK(self.lock);
    //模拟对共享资源处理的耗时
    [NSThread sleepForTimeInterval:1];
    NSLog(@"i:%d --- thread:%@ --- 将要处理共享资源",i,[NSThread currentThread]);
    [self.dict setObject:@"semaphore" forKey:@"key"];
    NSLog(@"i:%d --- thread:%@ --- 共享资源处理完成",i,[NSThread currentThread]);
    ZED_UNLOCK(self.lock);

    });
    }

    NSLog(@"resourceLockTask end --- thread:%@",[NSThread currentThread]);
    }

    在这一步中,我们使用异步执行并发任务会开辟新的线程的特性,来模拟开辟多个线程访问贡献资源的场景,同时使用了线程休眠的API来模拟对共享资源处理的耗时。这里我们开辟了3个线程来并发访问这个共享资源,代码运行的log如下:

    2019-04-27 18:36:25.275060+0800 GCD(四) dispatch_semaphore[35944:6315957] resourceLockTask start --- thread:<NSThread: 0x60000130e940>{number = 1, name = main}
    2019-04-27 18:36:25.275312+0800 GCD(四) dispatch_semaphore[35944:6315957] 异步添加任务:0
    2019-04-27 18:36:25.275508+0800 GCD(四) dispatch_semaphore[35944:6315957] 异步添加任务:1
    2019-04-27 18:36:25.275680+0800 GCD(四) dispatch_semaphore[35944:6315957] 异步添加任务:2
    2019-04-27 18:36:25.275891+0800 GCD(四) dispatch_semaphore[35944:6315957] resourceLockTask end --- thread:<NSThread: 0x60000130e940>{number = 1, name = main}
    2019-04-27 18:36:26.276757+0800 GCD(四) dispatch_semaphore[35944:6316211] i:0 --- thread:<NSThread: 0x6000013575c0>{number = 3, name = (null)} --- 将要处理共享资源
    2019-04-27 18:36:26.277004+0800 GCD(四) dispatch_semaphore[35944:6316211] i:0 --- thread:<NSThread: 0x6000013575c0>{number = 3, name = (null)} --- 共享资源处理完成
    2019-04-27 18:36:27.282099+0800 GCD(四) dispatch_semaphore[35944:6316212] i:1 --- thread:<NSThread: 0x600001357800>{number = 4, name = (null)} --- 将要处理共享资源
    2019-04-27 18:36:27.282357+0800 GCD(四) dispatch_semaphore[35944:6316212] i:1 --- thread:<NSThread: 0x600001357800>{number = 4, name = (null)} --- 共享资源处理完成
    2019-04-27 18:36:28.283769+0800 GCD(四) dispatch_semaphore[35944:6316214] i:2 --- thread:<NSThread: 0x600001369280>{number = 5, name = (null)} --- 将要处理共享资源
    2019-04-27 18:36:28.284041+0800 GCD(四) dispatch_semaphore[35944:6316214] i:2 --- thread:<NSThread: 0x600001369280>{number = 5, name = (null)} --- 共享资源处理完成

    从多次log中我们可以看出:

    添加信号锁之后,每个线程对于共享资源的操作都是有序的,并不会出现2个线程同时访问锁中的代码区域。

    我把上面的实现代码简化一下,方便分析这种锁的实现原理:

    //step_1
    ZED_LOCK(self.lock);
    //step_2
    NSLog(@"执行任务");
    //step_3
    ZED_UNLOCK(self.lock);

    1、信号量初始化的值为1,当一个线程过来执行step_1的代码时,会调用信号量的值减一的方法,这时,信号量的值为0,它会直接返回,然后执行step_2的代码去完成去共享资源的访问,然后再使用step_3中的signal方法让信号量加一,信号量的值又会回归到初始值1。这就是一个线程过来访问的调用流程。

    2、当线程1过来执行到step_2的时候,这时又有一个线程2它也从step_1处来调用这段代码,由于线程1已经调用过step_1的wait方法将信号量的值减一,这时信号量的值为0。同时线程2进入然后调用了step_1的wait方法又将信号量的值减一,这时的信号量的值为-1,由于信号量的值小于0时会阻塞当前线程(线程2),所以,线程2就会一直等待,直到线程1执行完step_3中的方法,将信号量加一,才会唤醒线程2,继续执行下面的代码。这就是为什么信号量可以对共享资源加锁的原因,如果我们可以允许n个线程同时访问,我们就需要在初始化这个信号量时把信号量的值设为n,这样就限制了访问共享资源的线程数。

    通过上面的分析,我们可以知道,如果我们使用信号量来进行线程同步时,我们需要把信号量的初始值设为0,如果要对资源加锁,限制同时只有n个线程可以访问的时候,我们就需要把信号量的初始值设为n。

    semaphore的释放

    在我们平常的开发过程中,如果对semaphore使用不当,就会在它释放的时候遇到奔溃问题。

    首先我们来看2个例子:

    - (IBAction)crashScene1:(UIButton *)sender {

    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    //在使用过程中将semaphore置为nil
    semaphore = nil;
    }
    - (IBAction)crashScene2:(UIButton *)sender {

    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    //在使用过程中对semaphore进行重新赋值
    semaphore = dispatch_semaphore_create(3);
    }

    我们打开测试代码,找到semaphore对应的target,然后运行一下代码,然后点击后面2个按钮调用一下上面的代码,然后我们可以发现,代码在运行到semaphore = nil;与semaphore = dispatch_semaphore_create(3);时奔溃了。然后我们使用lldb的bt命令查看一下调用栈。

    (lldb) bt
    * thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
    frame #0: 0x0000000111c31309 libdispatch.dylib`_dispatch_semaphore_dispose + 59
    frame #1: 0x0000000111c2fb06 libdispatch.dylib`_dispatch_dispose + 97
    * frame #2: 0x000000010efb113b GCD(四) dispatch_semaphore`-[ZEDDispatchSemaphoreViewController crashScene1:](self=0x00007fdcfdf0add0, _cmd="crashScene1:", sender=0x00007fdcfdd0a3d0) at ZEDDispatchSemaphoreViewController.m:117
    frame #3: 0x0000000113198ecb UIKitCore`-[UIApplication sendAction:to:from:forEvent:] + 83
    frame #4: 0x0000000112bd40bd UIKitCore`-[UIControl sendAction:to:forEvent:] + 67
    frame #5: 0x0000000112bd43da UIKitCore`-[UIControl _sendActionsForEvents:withEvent:] + 450
    frame #6: 0x0000000112bd331e UIKitCore`-[UIControl touchesEnded:withEvent:] + 583
    frame #7: 0x00000001131d40a4 UIKitCore`-[UIWindow _sendTouchesForEvent:] + 2729
    frame #8: 0x00000001131d57a0 UIKitCore`-[UIWindow sendEvent:] + 4080
    frame #9: 0x00000001131b3394 UIKitCore`-[UIApplication sendEvent:] + 352
    frame #10: 0x00000001132885a9 UIKitCore`__dispatchPreprocessedEventFromEventQueue + 3054
    frame #11: 0x000000011328b1cb UIKitCore`__handleEventQueueInternal + 5948
    frame #12: 0x0000000110297721 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
    frame #13: 0x0000000110296f93 CoreFoundation`__CFRunLoopDoSources0 + 243
    frame #14: 0x000000011029163f CoreFoundation`__CFRunLoopRun + 1263
    frame #15: 0x0000000110290e11 CoreFoundation`CFRunLoopRunSpecific + 625
    frame #16: 0x00000001189281dd GraphicsServices`GSEventRunModal + 62
    frame #17: 0x000000011319781d UIKitCore`UIApplicationMain + 140
    frame #18: 0x000000010efb06a0 GCD(四) dispatch_semaphore`main(argc=1, argv=0x00007ffee0c4efc8) at main.m:14
    frame #19: 0x0000000111ca6575 libdyld.dylib`start + 1
    frame #20: 0x0000000111ca6575 libdyld.dylib`start + 1
    (lldb)

    从上面的调用栈我们可以看出,奔溃的地方都处于libdispatch库调用dispatch_semaphore_dispose方法释放信号量的时候,为什么在信号量使用过程中对信号量进行重新赋值或置空操作会crash呢,这个我们就需要从GCD的源码层面来分析了,GCD的源码库libdispatch在苹果的开源代码库可以下载,我在自己的Github也放了一份libdispatch-187.10版本的,下面的源码分析都是基于这个版本的。

    首先我们来看一下dispatch_semaphore_t的结构体dispatch_semaphore_s的结构体定义

    struct dispatch_semaphore_s {
    DISPATCH_STRUCT_HEADER(dispatch_semaphore_s, dispatch_semaphore_vtable_s);
    long dsema_value; //当前的信号值
    long dsema_orig; //初始化的信号值
    size_t dsema_sent_ksignals;
    #if USE_MACH_SEM && USE_POSIX_SEM
    #error "Too many supported semaphore types"
    #elif USE_MACH_SEM
    semaphore_t dsema_port; //当前mach_port_t信号
    semaphore_t dsema_waiter_port; //休眠时mach_port_t信号
    #elif USE_POSIX_SEM
    sem_t dsema_sem;
    #else
    #error "No supported semaphore type"
    #endif
    size_t dsema_group_waiters;
    struct dispatch_sema_notify_s *dsema_notify_head;//链表头部
    struct dispatch_sema_notify_s *dsema_notify_tail;//链表尾部
    };

    这里我们需要关注2个值的变化,dsema_value与dsema_orig,它们分别代表当前的信号值与初始化时的信号值。

    当我们调用dispatch_semaphore_create方法创建信号量时,这个方法内部会把传入的参数存储到dsema_value(当前的value)和dsema_orig(初始value)中,条件是value的值必须大于或等于0。

    dispatch_semaphore_t
    dispatch_semaphore_create(long value)
    {
    dispatch_semaphore_t dsema;

    // If the internal value is negative, then the absolute of the value is
    // equal to the number of waiting threads. Therefore it is bogus to
    // initialize the semaphore with a negative value.
    if (value < 0) {//初始值不能小于0
    return NULL;
    }

    dsema = calloc(1, sizeof(struct dispatch_semaphore_s));//申请信号量的内存

    if (fastpath(dsema)) {//信号量初始化赋值
    dsema->do_vtable = &_dispatch_semaphore_vtable;
    dsema->do_next = DISPATCH_OBJECT_LISTLESS;
    dsema->do_ref_cnt = 1;
    dsema->do_xref_cnt = 1;
    dsema->do_targetq = dispatch_get_global_queue(
    DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dsema->dsema_value = value;//当前的值
    dsema->dsema_orig = value;//初始值
    #if USE_POSIX_SEM
    int ret = sem_init(&dsema->dsema_sem, 0, 0);//内存空间映射
    DISPATCH_SEMAPHORE_VERIFY_RET(ret);
    #endif
    }

    return dsema;
    }

    然后调用dispatch_semaphore_wait与dispatch_semaphore_signal时会对dsema_value做加一或减一操作。当我们对信号量置空或者重新赋值操作时,会调用dispatch_semaphore_dispose释放信号量,我们来看看对应的源码

    static void
    _dispatch_semaphore_dispose(dispatch_semaphore_t dsema)
    {
    if (dsema->dsema_value < dsema->dsema_orig) {//当前的信号值如果小于初始值就会crash
    DISPATCH_CLIENT_CRASH(
    "Semaphore/group object deallocated while in use");
    }

    #if USE_MACH_SEM
    kern_return_t kr;
    if (dsema->dsema_port) {
    kr = semaphore_destroy(mach_task_self(), dsema->dsema_port);
    DISPATCH_SEMAPHORE_VERIFY_KR(kr);
    }
    if (dsema->dsema_waiter_port) {
    kr = semaphore_destroy(mach_task_self(), dsema->dsema_waiter_port);
    DISPATCH_SEMAPHORE_VERIFY_KR(kr);
    }
    #elif USE_POSIX_SEM
    int ret = sem_destroy(&dsema->dsema_sem);
    DISPATCH_SEMAPHORE_VERIFY_RET(ret);
    #endif

    _dispatch_dispose(dsema);
    }

    从源码中我们可以看出,当dsema_value小于dsema_orig时,即信号量还在使用时,会直接调用DISPATCH_CLIENT_CRASH让APP奔溃。

    所以,我们在使用信号量的时候,不能在它还在使用的时候,进行赋值或者置空的操作。

    如果文中有错误的地方,或者与你的想法相悖的地方,请在评论区告知我,我会继续改进,如果你觉得这个篇文章总结的还不错,麻烦动动小手,给我的文章与Git代码样例点个✨

    链接:https://www.jianshu.com/p/7981e3357fe9

    收起阅读 »

    探究iOS鲜为人知的小秘密一一__attribute__运用

    Clang Attributes是Clang提供的一种源码注解,方便开发者向编译器表达某种要求,参与控制如Static Analyzer、Name Mangling、Code Generation等过程,一般以attribute(xxx)的形式出现在代码中;为...
    继续阅读 »

    Clang Attributes是Clang提供的一种源码注解,方便开发者向编译器表达某种要求,参与控制如Static Analyzer、Name Mangling、Code Generation等过程,一般以attribute(xxx)的形式出现在代码中;为方便使用,一些常用属性也被Cocoa定义成宏,比如在系统头文件中经常出现的NS_CLASS_AVAILABLE_IOS(9_0)就是attribute(availability(...))这个属性的简单写法。

    ※unavailable
    #define UNAVAILABLE_ATTRIBUTE __attribute__((unavailable))

    可以加在方法声明的后面,告诉编译器该方法不可用

    Swift中

    @available(*, unavailable)

    func foo() {}

    @available(iOS, unavailable, message="you can't call this")

    func foo2() {}

    ※availability

    #define NS_DEPRECATED_IOS(_iosIntro,_iosDep,...) CF_DEPRECATED_IOS(_iosIntro,_iosDep,__VA_ARGS__)

    展开看

    #define CF_DEPRECATED_IOS(_iosIntro, _iosDep, ...) __attribute__((availability(ios,introduced=_iosIntro,deprecated=_iosDep,message="" __VA_ARGS__)))

    再展开看

    __attribute__((availability(ios,introduced=2_0,deprecated=7_0,message=""__VA_ARGS__)))

    iOS即是iOS平台

    introduced从哪个版本开始使用

    deprecated从哪个版本开始弃用

    message警告的信息

    其实还可以再加一个参数例子:

    void f(void) __attribute__((availability(macosx,introduced=10.4,deprecated=10.6,obsoleted=10.7)));

    obsoleted完全禁止使用的版本

    NS_AVAILABLE_IOS(7_0) iOS7.0或之后才能使用

    NS_DEPRECATED_IOS(2_0,6_0) iOS2.0开始使用6.0废弃

    Swift中:

    @available(iOS 6.0, *)

    public var minimumScaleFactor: CGFloat

    @available(OSX, introduced=10.4, deprecated=10.6, obsoleted=10.10)

    @available(iOS, introduced=5.0, deprecated=7.0)

    func foo3() {}

    ※objc_subclassing_restricted

    一句话就是使用这个属性可以定义一个不可被继承的类

    __attribute__((objc_subclassing_restricted))

    @interface Eunuch : NSObject

    @end

    @interface Child : Eunuch//如果继承它会报错

    @end

    在Swift中对原来的很多attribute的支持现在还缺失中,为了达到类似的目的,我们可以使用一个final关键词


    ※objc_requires_super

    使用这个属性标志子类继承这个方法时需要调用super,否则给出编译警告,来让父类中有一些关键代码是在被继承重写后必须执行

    #define NS_REQUIRES_SUPER __attribute__((objc_requires_super))


    在Switf中也还是可以用final的方法来达到这个目的


    Swift equivalent to __attribute((objc_requires_super))?(stackoverflow)

    关于Swift中的final的详细讲解

    ※overloadable

    用于C函数,可以定义若干个相同函数名,但参数不同的方法,调用时编译器会自动根据参数选择函数去调用

    __attribute__((overloadable))

    void logAnything(id obj) {

    NSLog(@"%@", obj);

    }

    __attribute__((overloadable)) void logAnything(int number) {

    NSLog(@"%@", @(number));

    }

    __attribute__((overloadable)) void logAnything(CGRect rect) {

    NSLog(@"%@", NSStringFromCGRect(rect));

    }

    // Tests

    logAnything(@[@"1", @"2"]);

    logAnything(233);

    logAnything(CGRectMake(1, 2, 3, 4));

    有兴趣的可以写一个自定义的Log免去用NSLog要写@""等格式的麻烦

    ※cleanup

    __attribute__((cleanup(...))),用于修饰一个变量,在它的作用域结束时可以自动执行一个指定的方法,个人感觉这个真的很方便


    一个对象可以这样用,那么block实际也是一个对象,这样就可以写一个宏,实际上就是ReactiveCocoa中神奇的@onExit

    #define onExit\

    __strong void(^block)(void) __attribute__((cleanup(blockCleanUp), unused)) = ^

    有时候我们需要用到锁这个东西,或者一些CoreFoundation的对象有时候最后需要释放,用这个宏就很方便了


    为了好看用这个宏的时候加个@就加个释放池就可以了


    sunnyxx这篇博客讲的很清楚

    Swift中:可以用defer


    还有一些format, const, noreturn, aligned , packed, objc_boxable, constructor / destructor, enable_if, objc_runtime_name可以看这两篇文章:

    神奇的attribute

    Clang Attributes黑魔法小记

    链接:https://www.jianshu.com/p/33d7d0596028

    收起阅读 »

    一步步封装实现自己的网络请求框架 3.0

    一、ReactiveHtt协程这个概念已经出现很多年了,但 Kotlin 协程是在 2018 年才发布了 1.0 版本,被 Android 开发者所熟知还要再往后一段时间,协程的意义不是本篇文章所应该探讨的,但如果你去了解下协程能给我们带来的开发效益,我相信你...
    继续阅读 »
    一、ReactiveHtt

    协程这个概念已经出现很多年了,但 Kotlin 协程是在 2018 年才发布了 1.0 版本,被 Android 开发者所熟知还要再往后一段时间,协程的意义不是本篇文章所应该探讨的,但如果你去了解下协程能给我们带来的开发效益,我相信你是会喜欢上它的

    闲话说完,这里先贴上 3.0 版本的 GitHub 链接:ReactiveHttp

    3.0 版本的技术栈已更新为了 Kotlin + Jetpack + Coroutines + Retrofit,已托管到 jitpack.io,感兴趣的读者可以直接远程导入依赖

    allprojects {
    repositories {
    maven { url 'https://jitpack.io' }
    }
    }

    dependencies {
    implementation 'com.github.leavesC:ReactiveHttp:latest_version'
    }


    二、能给你带来什么

    ReactiveHttp 能够给你带来什么?

    • 更现代化的技术栈。Kotlin + Jetpack + Retrofit 现在应该是大多数 Android 开发者选用的最基础组件了,Kotlin 协程会相对比较少人接触,但我觉得协程也是未来的主流方向之一,毕竟连 Retrofit 也原生支持 Kotlin 协程了,本库会更加符合大多数开发者的现实需求
    • 极简的设计理念。ReactiveHttp 目前仅包含十二个 Kotlin 文件,设计上遵循约定大于配置的理念,大多数配置项都可以通过方法复写的形式来实现自定义
    • 极简的使用方式。只需要持有一个 RemoteDataSource 对象,开发者就可以在任意地方发起异步请求和同步请求了。此外,进行网络请求 Callback 自然是必不可少的,ReactiveHttp 提供了多个事件回调:onStart、onSuccess、onSuccessIO、onFailed、onFailToast、onCancelled、onFinally 等,按需声明即可,甚至可以完全不用实现
    • 支持通用性的自动化行为。对于网络请求来说,像 showLoading、dismissLoading、showToast 等行为是具有通用性的,我们肯定不希望每个网络请求都要来手动调用方法触发以上操作,而是希望能够在发起网络请求的过程中自动完成。ReactiveHttp 就提供了自动完成以上通用性 UI 行为的功能 ,且每个操作均和生命周期相绑定,避免了常见的内存泄漏和 NPE 问题,并提供了交由外部使用者来自定义各种其它行为的入口
    • 极低的接入成本。ReactiveHttp 并不强制要求外部必须继承于任何 BaseViewModel 或者是 BaseActivity/BaseFragment,外部只要通过实现 IUIActionEventObserverIViewModelActionEvent 两个接口即可享受 ReactiveHttp 带来的各个益处。当然,如果你不需要 ReactiveHttp 的自动化行为的话,也可以不实现任何接口
    • 支持多个(两个/三个)接口同时并发请求,在网络请求成功时同步回调,所以理论上多个接口的总耗时就取决于耗时最长的那个接口,从而缩短用户的等待时间,提升用户体验

    ReactiveHttp 不能带给你什么?

    • ReactiveHttp 本身的应用领域是专注于接口请求的,所以对于接口的返回值形式带有强制约束,且没有封装文件下载、文件上传等功能
    • 肯定有,但还没想到

    ReactiveHttp 目前已经在我司项目上稳定运行一年多了,在这过程中我也是在逐步优化,使其能够更加适用于外部不同环境的需求,到目前为止我觉得也是已经足够稳定了,希望对你有所帮助 😇😇

    三、架构说明

    现在应该有百分之九十以上的 Android 客户端的网络请求是直接或间接依靠 OkHttp 来完成的吧?本文所说的网络请求框架就是指在 OkHttp 之上所做的一层封装。原生的 OkHttp 在使用上并不方便,甚至可以说是有点繁琐。Retrofit 在易用性上有所提升,但是如果直接使用的话也并不算多简洁。所以我们往往都是会根据项目架构自己来对 OkHttp 或者 Retrofit 进行多一层封装,ReactiveHttp 的实现出发点即是如此

    此外,现在大多数项目都引用到了 Jetpack 这一套组件来实现 MVVM 架构的吧?ReactiveHttp 将 Jetpack 和 Retrofit 关联了起来,使得网络请求过程更加符合“响应式”概念,并提供了更加可靠的生命周期安全性和自动化行为

    一般的网络请求框架是只专注于完成网络请求并透传出结果,ReactiveHttp 不太一样,ReactiveHttp 在这个基础上还实现了将网络请求和 ViewModel 以及 Activity/Fragment 相绑定的功能,ReactiveHttp 希望做到的是能够尽量完成大多数的通用行为,且每个层次均不强依赖于特定父类

    Google 官方曾推出过一份最佳应用架构指南。当中,每个组件仅依赖于其下一级的组件。ViewModel 是关注点分离原则的一个具体实现,是作为用户数据的承载体处理者而存在的,Activity/Fragment 仅依赖于 ViewModel,ViewModel 就用于响应界面层的输入和驱动界面层变化,Repository 用于为 ViewModel 提供一个单一的数据来源及数据存储域,Repository 可以同时依赖于持久性数据模型和远程服务器数据源

    ReactiveHttp 的设计思想类似于 Google 推荐的最佳应用架构指南

    • BaseRemoteDataSource 作为数据提供者处于最下层,只用于向上层提供数据,提供了多个同步请求和异步请求方法,和 BaseReactiveViewModel 之间依靠 IUIActionEvent 接口来联系
    • BaseReactiveViewModel 作为用户数据的承载体和处理者,包含了多个和网络请求事件相关的 LiveData 用于驱动界面层的 UI 变化,和 BaseReactiveActivity 之间依靠 IViewModelActionEvent 接口来联系
    • BaseReactiveActivity 包含与系统和用户交互的逻辑,其负责响应 BaseReactiveViewModel 中的数据变化,提供了和 BaseReactiveViewModel 进行绑定的方法

    上文有说到,ReactiveHttp 提供了在网络请求过程中自动完成 showLoading、dismissLoading、showToast 等行为的能力。首先,BaseRemoteDataSource 在网络请求过程中会通过 IUIActionEvent 接口来通知 BaseReactiveViewModel 需要触发的行为,从而连锁触发 ShowLoadingLiveData、DismissLoadingLiveData、ShowToastLiveData 值的变化,BaseReactiveActivity 就通过监听 LiveData 值的变化来完成 UI 层操作

    四、惯常做法

    以下步骤应该是大部分应用目前进行网络请求时的惯常做法了

    服务端返回给移动端的数据使用具有特定格式的 Json 来进行通信,用整数 status 来标明本次请求是否成功,在失败时则直接 showToast(msg)data则需要用泛型来声明了,最终就对应移动端的一个泛型类,类似于 HttpWrapBean

    {
    "status":200,
    "msg":"success",
    "data":""
    }

    data class HttpWrapBean(val status: Int, val msg: String, val data: T)

    interface ApiService {

    @POST("api1")
    fun api1(): ObservableInt>>

    @GET("api2")
    fun api2(): Call>

    }


    然后在 interface 中声明 Api 接口,这也是使用 Retrofit 的惯常用法。根据项目中的实际情况,开发者可能是使用 Call 或者 Observable 作为每个接口返回值的最外层的数据包装类,然后再使用 HttpWrapBean 来作为具体数据类的包装类

    然后项目中使用的是 RxJava,那么就需要像以下这样来完成网络请

    val retrofit = Retrofit.Builder()
    .baseUrl("https://xxx.com")
    .addConverterFactory(GsonConverterFactory.create())
    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
    .build()
    val service = retrofit.create(ApiService::class.java)
    val call: ObservableInt>> = service.api1()
    call.subscribe(object : ConsumerInt>> {
    override fun accept(userBean: HttpWrapBean?) {

    }

    }, object : Consumer {
    override fun accept(t: Throwable?) {

    }
    })


    五、简单入门

    ReactiveHttp 在使用上会比上面给出的例子简单很多,下面就来看下通过 ReactiveHttp 如何完成网络请求

    ReactiveHttp 需要知道网络请求的结果,但不知道外部会使用什么字段名来标识 HttpWrapBean 中的三个值,所以需要外部实现 IHttpWrapBean 接口来进行标明。例如,你可以这样来实现:

    data class HttpWrapBean(val status: Int, val msg: String, val data: T) : IHttpWrapBean {

    override val httpCode: Int
    get() = status

    override val httpMsg: String
    get() = msg

    override val httpData: T
    get() = data

    //网络请求是否成功
    override val httpIsSuccess: Boolean
    get() = status == 200

    }


    suspend来修饰接口方法,且不需要其它的外层包装类。suspend是 kotlin 协程引入的,当用该关键字修饰接口方法时,Retrofit 内部就会使用协程的方式来完成该网络请求

    interface ApiService {

    @GET("config/district")
    suspend fun getProvince(): HttpWrapBean>

    }


    ReactiveHttp 提供了 RemoteExtendDataSource 交由外部来继承实现。RemoteExtendDataSource 包含了所有的网络请求方法,外部仅需要根据实际情况来实现三个必要的字段和方法即可

    • releaseUrl。即应用的 BaseUrl
    • createRetrofit。用于创建 Retrofit,开发者可以在这里自定义 OkHttpClient
    • showToast。当网络请求失败时,通过该方法来向用户提示失败原因

    例如,你可以像以下这样来实现你自己项目的专属 DataSource,当中就包含了开发者整个项目的全局网络请求配置

    class SelfRemoteDataSource(iActionEvent: IUIActionEvent?) : RemoteExtendDataSource(iActionEvent, ApiService::class.java) {

    companion object {

    private val httpClient: OkHttpClient by lazy {
    createHttpClient()
    }

    private fun createHttpClient(): OkHttpClient {
    val builder = OkHttpClient.Builder()
    .readTimeout(1000L, TimeUnit.MILLISECONDS)
    .writeTimeout(1000L, TimeUnit.MILLISECONDS)
    .connectTimeout(1000L, TimeUnit.MILLISECONDS)
    .retryOnConnectionFailure(true)
    .addInterceptor(FilterInterceptor())
    .addInterceptor(MonitorInterceptor(MainApplication.context))
    return builder.build()
    }

    }

    /**
    * 由子类实现此字段以便获取 release 环境下的接口 BaseUrl
    */
    override val releaseUrl: String
    get() = "https://restapi.amap.com/v3/"

    /**
    * 允许子类自己来实现创建 Retrofit 的逻辑
    * 外部无需缓存 Retrofit 实例,ReactiveHttp 内部已做好缓存处理
    * 但外部需要自己判断是否需要对 OKHttpClient 进行缓存
    * @param baseUrl
    */
    override fun createRetrofit(baseUrl: String): Retrofit {
    return Retrofit.Builder()
    .client(httpClient)
    .baseUrl(baseUrl)
    .addConverterFactory(GsonConverterFactory.create())
    .build()
    }

    override fun showToast(msg: String) {
    Toast.makeText(MainApplication.context, msg, Toast.LENGTH_SHORT).show()
    }

    }


    之后,我们就可以依靠 SelfRemoteDataSource 在任意地方发起网络请求了,按需声明 Callback 方法。此外,由于使用到了扩展函数,所以 SelfRemoteDataSource 中可以直接调用 ApiService 中的接口方法,无需特意引用和导包

    六、进阶使用

    上述在使用 SelfRemoteDataSource 发起网络请求时虽然调用的是 enqueueLoading 方法,但实际上并不会弹出 loading 框,因为完成 ShowLoading、DismissLoading、ShowToast 等 UI 行为是需要 RemoteDataSource、ViewModel 和 Activity 这三者一起进行配合的,即 SelfRemoteDataSource 需要和其它两者关联上,将需要触发的 UI 行为反馈给 Activity

    这可以通过直接继承于 BaseReactiveActivity 和 BaseReactiveViewModel 来实现,也可以通过实现相应接口来完成关联。当然,如果你不需要 ReactiveHttp 的各个自动化行为的话,也可以不做以下任何改动

    总的来说,ReactiveHttp 具有极低的接入成本

    1、BaseReactiveActivity

    BaseReactiveActivity 是 ReactiveHttp 提供的一个默认 BaseActivity,其实现了 IUIActionEventObserver 接口,用于提供一些默认参数和默认行为,例如 CoroutineScope 和 showLoading。但在大多数情况下,我们自己的项目是不会去继承外部 Activity 的,而是会有一个自己实现的全局统一的 BaseActivity,所以如果你不想继承 BaseReactiveActivity 的话,可以自己来实现 IUIActionEventObserver 接口,就像以下这样

    @SuppressLint("Registered")
    abstract class BaseActivity : AppCompatActivity(), IUIActionEventObserver {

    protected inline fun getViewModel(
    factory: ViewModelProvider.Factory? = null,
    noinline initializer: (VM.(lifecycleOwner: LifecycleOwner) -> Unit)? = null
    ): Lazy where VM : ViewModel, VM : IViewModelActionEvent {
    return getViewModel(VM::class.java, factory, initializer)
    }

    override val lifecycleSupportedScope: CoroutineScope
    get() = lifecycleScope

    override val lContext: Context?
    get() = this

    override val lLifecycleOwner: LifecycleOwner
    get() = this

    private var loadDialog: ProgressDialog? = null

    override fun showLoading(job: Job?) {
    dismissLoading()
    loadDialog = ProgressDialog(lContext).apply {
    setCancelable(true)
    setCanceledOnTouchOutside(false)
    //用于实现当弹窗销毁的时候同时取消网络请求
    // setOnDismissListener {
    // job?.cancel()
    // }
    show()
    }
    }

    override fun dismissLoading() {
    loadDialog?.takeIf { it.isShowing }?.dismiss()
    loadDialog = nul
    }


    2、BaseReactiveViewModel

    类似地,BaseReactiveViewModel 是 ReactiveHttp 提供的一个默认的 BaseViewModel,其实现了 IViewModelActionEvent 接口,用于接收 RemoteDataSource 发起的 UI 层行为。如果你不希望继承于 BaseReactiveViewModel 的话,可以自己来实现 IViewModelActionEvent 接口,就像以下这样

    open class BaseViewModel : ViewModel(), IViewModelActionEvent {

    override val lifecycleSupportedScope: CoroutineScope
    get() = viewModelScope

    override val showLoadingEventLD = MutableLiveData()

    override val dismissLoadingEventLD = MutableLiveData()

    override val showToastEventLD = MutableLiveData()

    override val finishViewEventLD = MutableLiveData()

    }


    3、关联上

    完成以上两步后,开发者就可以像如下所示这样将 RemoteDataSource、ViewModel 和 Activity 这三者给关联起来。WeatherActivity 通过 getViewModel 方法来完成 WeatherViewModel 的初始化和内部多个 UILiveData 的绑定,并在 lambda 表达式中完成对 WeatherViewModel 内部和具体业务相关的 DataLiveData 的数据监听,至此所有自动化行为就都已经绑定上了

    class WeatherViewModel : BaseReactiveViewModel() {

    private val remoteDataSource by lazy {
    SelfRemoteDataSource(this)
    }

    val forecastsBeanLiveData = MutableLiveData()

    fun getWeather(city: String) {
    remoteDataSource.enqueue({
    getWeather(city)
    }) {
    onSuccess {
    if (it.isNotEmpty()) {
    forecastsBeanLiveData.value = it[0]
    }
    }
    }
    }

    }

    class WeatherActivity : BaseReactiveActivity() {

    private val weatherViewModel by getViewModel {
    forecastsBeanLiveData.observe(this@WeatherActivity, {
    showWeather(it)
    })
    }

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_weather)
    weatherViewModel.getWeather("adCode")
    }

    private fun showWeather(forecastsBean: ForecastsBean) {

    }

    }


    七、其它

    1、BaseRemoteDataSource

    RemoteExtendDataSource 提供了许多个可以进行复写的方法,既可用于配置 OkHttp 的各个网络请求参数,也用于交由外部进行流程控制。例如,你可以这样来实现自己项目的 BaseRemoteDataSource

    class BaseRemoteDataSource(iActionEvent: IUIActionEvent?) : RemoteExtendDataSource(iActionEvent, ApiService::class.java) {

    companion object {

    private val httpClient: OkHttpClient by lazy {
    createHttpClient()
    }

    private fun createHttpClient(): OkHttpClient {
    val builder = OkHttpClient.Builder()
    .readTimeout(1000L, TimeUnit.MILLISECONDS)
    .writeTimeout(1000L, TimeUnit.MILLISECONDS)
    .connectTimeout(1000L, TimeUnit.MILLISECONDS)
    .retryOnConnectionFailure(true)
    .addInterceptor(FilterInterceptor())
    .addInterceptor(MonitorInterceptor(MainApplication.context))
    return builder.build()
    }
    }

    /**
    * 由子类实现此字段以便获取 release 环境下的接口 BaseUrl
    */
    override val releaseUrl: String
    get() = HttpConfig.BASE_URL_MAP

    /**
    * 允许子类自己来实现创建 Retrofit 的逻辑
    * 外部无需缓存 Retrofit 实例,ReactiveHttp 内部已做好缓存处理
    * 但外部需要自己判断是否需要对 OKHttpClient 进行缓存
    * @param baseUrl
    */
    override fun createRetrofit(baseUrl: String): Retrofit {
    return Retrofit.Builder()
    .client(httpClient)
    .baseUrl(baseUrl)
    .addConverterFactory(GsonConverterFactory.create())
    .build()
    }

    /**
    * 如果外部想要对 Throwable 进行特殊处理,则可以重写此方法,用于改变 Exception 类型
    * 例如,在 token 失效时接口一般是会返回特定一个 httpCode 用于表明移动端需要去更新 token 了
    * 此时外部就可以实现一个 BaseException 的子类 TokenInvalidException 并在此处返回
    * 从而做到接口异常原因强提醒的效果,而不用去纠结 httpCode 到底是多少
    */
    override fun generateBaseException(throwable: Throwable): BaseHttpException {
    return if (throwable is BaseHttpException) {
    throwable
    } else {
    LocalBadException(throwable)
    }
    }

    /**
    * 用于由外部中转控制当抛出异常时是否走 onFail 回调,当返回 true 时则回调,否则不回调
    * @param httpException
    */
    override fun exceptionHandle(httpException: BaseHttpException): Boolean {
    return true
    }

    /**
    * 用于将网络请求过程中的异常反馈给外部,以便记录
    * @param throwable
    */
    override fun exceptionRecord(throwable: Throwable) {
    Log.e("SelfRemoteDataSource", throwable.message ?: "")
    }

    /**
    * 用于对 BaseException 进行格式化,以便在请求失败时 Toast 提示错误信息
    * @param httpException
    */
    override fun exceptionFormat(httpException: BaseHttpException): String {
    return when (httpException.realException) {
    null -> {
    httpException.errorMessage
    }
    is ConnectException, is SocketTimeoutException, is UnknownHostException -> {
    "连接超时,请检查您的网络设置"
    }
    else -> {
    "请求过程抛出异常:" + httpException.errorMessage
    }
    }
    }

    override fun showToast(msg: String) {
    Toast.makeText(MainApplication.context, msg, Toast.LENGTH_SHORT).show()
    }

    }


    此外,开发者可以直接在自己的 BaseViewModel 中声明一个 BaseRemoteDataSource 变量实例,所有子 ViewModel 都全局统一使用同一份 DataSource 配置。如果有某些特定接口需要使用不同的 BaseUrl 的话,也可以再多声明一个 BaseRemoteDataSource

    open class BaseViewModel : BaseReactiveViewModel() {

    /**
    * 正常来说单个项目中应该只有一个 RemoteDataSource 实现类,即全局使用同一份配置
    * 但父类也应该允许子类使用一个独有的 RemoteDataSource,即允许子类复写此字段
    */
    protected open val remoteDataSource by lazy {
    BaseRemoteDataSource(this)
    }

    }


    2、BaseHttpException

    BaseHttpException 是 ReactiveHttp 对网络请求过程中发生的各类异常情况的包装类,任何透传到外部的异常信息均会被封装为 BaseHttpException 类型。BaseHttpException 有两个默认子类,分别用于表示服务器异常和本地异

    /**
    * @param errorCode 服务器返回的错误码 或者是 HttpConfig 中定义的本地错误码
    * @param errorMessage 服务器返回的异常信息 或者是 请求过程中抛出的信息,是最原始的异常信息
    * @param realException 用于当 code 是本地错误码时,存储真实的运行时异常
    */
    open class BaseHttpException(val errorCode: Int, val errorMessage: String, val realException: Throwable?) : Exception(errorMessage) {

    companion object {

    /**
    * 此变量用于表示在网络请求过程过程中抛出了异常
    */
    const val CODE_ERROR_LOCAL_UNKNOWN = -1024520

    }

    /**
    * 是否是由于服务器返回的 code != successCode 导致的异常
    */
    val isServerCodeBadException: Boolean
    get() = this is ServerCodeBadException

    /**
    * 是否是由于网络请求过程中抛出的异常(例如:服务器返回的 Json 解析失败)
    */
    val isLocalBadException: Boolean
    get() = this is LocalBadException

    }

    /**
    * API 请求成功了,但 code != successCode
    * @param errorCode
    * @param errorMessage
    */
    class ServerCodeBadException(errorCode: Int, errorMessage: String) : BaseHttpException(errorCode, errorMessage, null) {

    constructor(bean: IHttpWrapBean<*>) : this(bean.httpCode, bean.httpMsg)

    }

    /**
    * 请求过程抛出异常
    * @param throwable
    */
    class LocalBadException(throwable: Throwable) : BaseHttpException(CODE_ERROR_LOCAL_UNKNOWN, throwable.message?: "", throwable)


    有时候开发者需要对某些异常情况进行特殊处理,此时就可以来实现自己的 BaseHttpException 子类。例如,在 token 失效时接口一般是会返回特定一个 httpCode 用于表明移动端需要去更新 token 了,此时开发者就可以实现一个 BaseHttpException 的子类 TokenInvalidException 并在 BaseRemoteDataSource 中进行返回,从而做到接口异常原因强提醒的效果,而不用去纠结 httpCode 到底是多少

    class TokenInvalidException : BaseHttpException(CODE_TOKEN_INVALID, "token已失效", null)

    open class BaseRemoteDataSource(iActionEvent: IUIActionEvent?) : RemoteExtendDataSource(iActionEvent, ApiService::class.java) {

    companion object {

    private val httpClient: OkHttpClient by lazy {
    createHttpClient()
    }

    private fun createHttpClient(): OkHttpClient {
    val builder = OkHttpClient.Builder()
    .readTimeout(1000L, TimeUnit.MILLISECONDS)
    .writeTimeout(1000L, TimeUnit.MILLISECONDS)
    .connectTimeout(1000L, TimeUnit.MILLISECONDS)
    .retryOnConnectionFailure(true)
    .addInterceptor(FilterInterceptor())
    .addInterceptor(MonitorInterceptor(MainApplication.context))
    return builder.build()
    }
    }

    /**
    * 由子类实现此字段以便获取 release 环境下的接口 BaseUrl
    */
    override val releaseUrl: String
    get() = "https://restapi.amap.com/v3/"

    /**
    * 允许子类自己来实现创建 Retrofit 的逻辑
    * 外部无需缓存 Retrofit 实例,ReactiveHttp 内部已做好缓存处理
    * 但外部需要自己判断是否需要对 OKHttpClient 进行缓存
    * @param baseUrl
    */
    override fun createRetrofit(baseUrl: String): Retrofit {
    return Retrofit.Builder()
    .client(httpClient)
    .baseUrl(baseUrl)
    .addConverterFactory(GsonConverterFactory.create())
    .build()
    }

    /**
    * 如果外部想要对 Throwable 进行特殊处理,则可以重写此方法,用于改变 Exception 类型
    * 例如,在 token 失效时接口一般是会返回特定一个 httpCode 用于表明移动端需要去更新 token 了
    * 此时外部就可以实现一个 BaseException 的子类 TokenInvalidException 并在此处返回
    * 从而做到接口异常原因强提醒的效果,而不用去纠结 httpCode 到底是多少
    */
    override fun generateBaseException(throwable: Throwable): BaseHttpException {
    if (throwable is ServerCodeBadException && throwable.errorCode == BaseHttpException.CODE_TOKEN_INVALID) {
    return TokenInvalidException()
    }
    return if (throwable is BaseHttpException) {
    throwable
    } else {
    LocalBadException(throwable)
    }
    }

    /**
    * 用于由外部中转控制当抛出异常时是否走 onFail 回调,当返回 true 时则回调,否则不回调
    * @param httpException
    */
    override fun exceptionHandle(httpException: BaseHttpException): Boolean {
    return httpException !is TokenInvalidException
    }

    override fun showToast(msg: String) {
    Toast.makeText(MainApplication.context, msg, Toast.LENGTH_SHORT).show()
    }
    }


    收起阅读 »

    我愿赌上一包辣条,这些定位相关你不知道

    写码写累了不如来看看这些奇奇怪怪的css,腰不酸腿不疼,一阵轻松加愉快(还能偷我表情包)子元素的绝对定位原点在哪?用了那么久的定位,有注意过子元素的绝对定位原点,是在盒子模型的哪一处,在 padding、border 还是 content?实践出真知。既然不确...
    继续阅读 »

    写码写累了不如来看看这些奇奇怪怪的css,腰不酸腿不疼,一阵轻松加愉快(还能偷我表情包)

    子元素的绝对定位原点在哪?

    用了那么久的定位,有注意过子元素的绝对定位原点,是在盒子模型的哪一处,在 padding、border 还是 content?


    实践出真知。既然不确定那就实操个例子看看。

    <div class="father">
    father
    <div class="child">child</div>
    </div>
    body {
    background-color: rgb(20, 19, 19);
    }

    .father {
    width: 300px;
    height: 300px;
    margin: 40px;
    border: 20px solid rgb(202, 30, 30);
    padding: 40px;
    position: relative;
    background-color: #eee;
    }

    .child {
    width: 50px;
    height: 50px;
    position: absolute;
    top: 0;
    left: 0;
    background-color: rgb(228, 207, 17);
    }

    在 Chrome 90 的版本下的表现。



    更换 Edge、火狐、IE 浏览器,以及设置 box-sizing 分别为 border-boxcontent-box,最后的结果都表现一致。


    从结果上看,绝对定位的字元素是紧贴着父元素的内边框,绝对定位的原点就是在父元素 padding 的左上角。


    如果绝对定位的父亲们都没有设置 relative,那么是将会是定位在哪?

    body 下只有一个绝对定位的元素,设置了 bottom:0,那么他的表现将会是如何呢?定位在 body 的底边?

    <body>
    I am body
    <div class="absolute">I am absoluted</div>
    </body>
    html {
    background-color: #fff;
    }

    body {
    height: 50vh;
    background-color: #ddd;
    }
    .absolute {
    width: 120px;
    height: 50px;
    position: absolute;
    bottom: 0;
    left: 0;
    background-color: rgb(0, 0, 0);
    color: #fff;
    }


    从结果上看,绝对定位的元素并不是相对于 body 进行定位的,也不是根据 html 标签,此时的 html 的宽高等同于 body 的宽高,而是根据浏览器视口进行定位的。


    所有父元素position 的属性是static 的时候,绝对定位的元素会被包含在初始包含块中,初始包含块有着和浏览器视口一样的大小,所以从表现上来看,就是绝对定位的元素是根据浏览器视口定位。


    如果把top和left去掉,那么位置依旧是他原来文档流的位置,只是不占空间了,后面的元素会窜上来。

    通过 HTML 结构控制层叠上下文

    在使用定位属性时,必不可少的使用 z-index 属性,使用 z-index 属性会创建一个层叠上下文。z-index 值不会在整个文档中进行比较,而只会在该层叠上下文中进行比较。

    <div class="bar"></div>
    <div class="container">
    <div class="bottom-box-1">
    <div class="top-box"></div>
    </div>
    </div>
    <div class="container">
    <div class="box-container">
    <div class="bottom-box-2"></div>
    <div class="top-box"></div>
    </div>
    </div>
    body {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 90vh;
    }
    .bar {
    position: absolute;
    top: 65vh;
    z-index: 2;
    width: 100vw;
    height: 20px;
    background: #8bbe6e;
    }

    .top-box {
    position: absolute;
    z-index: 3;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 50%;
    height: 50%;
    background: #626078;
    filter: brightness(60%);
    }

    .bottom-box-1 {
    position: absolute;
    top: 45%;
    z-index: 1;
    transition: top 1s;
    width: 40px;
    height: 40px;
    background: #626078;
    }

    .container:hover .bottom-box-1 {
    top: 72%;
    }

    .box-container {
    position: absolute;
    top: 45%;
    transition: top 1s;
    }

    .bottom-box-2 {
    position: relative;
    z-index: 1;
    width: 40px;
    height: 40px;
    background: #626078;
    }

    .container {
    border: 2px dashed #626078;
    height: 80%;
    width: 100px;
    margin: 20px;
    padding: 10px;
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    }

    .container:hover .box-container {
    top: 72%;
    }


    鼠标分别移入两个虚线框内,我们发现,第二个例子 bar 穿过了两个正方形。


    这两者的区别就在于 HTML 结构,在第一个例子中,小正方形在大正方形的里面,大正方形在移动的时候,小正方形也随之移动,但是因为大正方形对决定位且有 z-index 属性不为 auto,因此创建了一个层叠上下文,这导致大正方形内的所有元素都是在这个层叠上下文里层叠。


    那么第二个例子是怎么解决的呢?


    第二个例子的技巧在于引入了一个新的 div 来包裹这两个正方形,这个新的 div 只负责移动。而里面的大小正方形和 bar 处于同一个层叠上下文中。这样子就可以产生 bar 从两个正方形中穿过的效果。


    还没懂的来看图来看图:


    总结一下创建层叠上下文的几种情况(别怪我枯燥,就是这么多):



    • 文档根元素<html>;

    • position 值为 relative(相对定位)或 absolute(绝对定位)且 z-index 值不为 auto 的元素;

    • position 值为 fixed(固定定位)或 sticky(粘滞定位)的元素;

    • flex (flexbox) 容器的子元素,且 z-index 值不为 auto;

    • grid 容器的子元素,且 z-index 值不为 auto;

    • opacity 属性值小于 1 的元素;

    • mix-blend-mode 属性值不为 normal 的元素;

    • 以下任意属性值不为 none 的元素:

      • transform

      • filter

      • perspective

      • clip-path

      • mask / mask-image / mask-border



    • isolation 属性值为 isolate 的元素;

    • -webkit-overflow-scrolling 属性值为 touch 的元素;

    • will-change 值设定了任一属性而该属性在 non-initial 值时会创建层叠上下文的元素

    • contain 属性值为 layoutpaint 或包含它们其中之一的合成值(比如 contain: strictcontain: content)的元素;


    好了,你学废了嘛~


    当定位遇到 Transform

    transform 下 absolute 宽度被限制

    以前,我们设置 absolute 元素宽度 100%, 则都会参照第一个非static值的position祖先元素计算,没有就window. 现在,=-=,需要把transform也考虑在内了。


    默认情况下我们设置 absolute 的宽度 100%,会根据第一个不是static的祖先元素计算,没有就找视口宽度。现在也考虑 CSS3 的 transform 属性了。


    <div class="relative">
    <div class="transform">
    <div class="absolute">i am in transform</div>
    </div>
    </div>
    <div class="relative">
    <div class="no-transform">
    <div class="absolute">i am not in transform</div>
    </div>
    </div>
    .relative {
    position: relative;
    width: 400px;
    height: 100px;
    background-color: rgb(233, 233, 233);
    }

    .transform {
    transform: rotate(0);
    width: 200px;
    }

    .no-transform {
    width: 200px;
    }

    .absolute {
    position: absolute;
    width: 100%;
    height: 100px;
    background-color: rgb(137, 174, 255);
    }


    可以看到绝对定位的宽度是相对 transform 的大小计算了。

    transform 对 fixed 的限制

    身为大聪明的你,加了一行position:fixed安心上线,结果预览机一瞅,没生效??

    position:fixed 正常情况下可以让元素不跟随滚动条滚动,这种行为也无法通过 relative/absolute 来限制。但是遇到 transform,他就被打败了,降级成 absolute
    <div class="demo">
    <div class="box">
    <div class="fixed">
    <p>没有transform</p>
    <img src="https://gitee.com/zukunft/MDImage/raw/master/img/20210516172759.jpg" alt="">
    </div>
    </div>

    <div class="relative box">
    <div class="fixed">
    <p> 有relative</p>
    <img src="https://gitee.com/zukunft/MDImage/raw/master/img/20210516172759.jpg" alt="">
    </div>
    </div>
    <div class="transform box">
    <div class="fixed">
    <p>有transform</p>
    <img src="https://gitee.com/zukunft/MDImage/raw/master/img/20210516172759.jpg" alt="">
    </div>
    </div>
    </div>
    .box {
    height: 250px;
    }

    .demo {
    height: 9999px;
    }


    .fixed {
    position: fixed;
    }

    .relative {
    position: relative;
    }

    .transform {
    transform: scale(1);
    }


    诶,神奇不,滚起来了,就只有被transform包裹的元素会被滚走。
    根据W3C的定义,transform属性值会使元素成为一个包含块,它的后代包括absolute元素,fixed元素受限在其 padding box 区域。所以滚动的时候,transform元素被滚走,其子元素也跟随tranform滚走。


    关于包含块

    上面提到了包含块,那到底如何形成的包含块,包含块又是个啥子

    在 MDN 中的解释

    The size and position of an element are often impacted by its containing block. Percentage values that are applied to the width, height, padding, margin, and offset properties of an absolutely positioned element (i.e., which has its positionset to absolute or fixed) are computed from the element's containing block.
    即一个元素的尺寸和位置受到它的**包含块(containing block)**的影响。对于一些属性,例如 width, height, padding, margin,绝对定位元素的偏移值 (比如 position 被设置为 absolutefixed),当我们设置百分比值的时候,它的这些值计算,就是通过该元素的包含块的值来计算的。

    通常情况下,包含块就是这个元素最近的祖先块元素的内容区域,但实际可能不是;


    我们可以通过 position 的属性来确定它的包含块;



    1. 如果 position 属性为 staticrelativesticky,包含块可能由它的最近的祖先块元素(比如说 inline-block, blocklist-item 元素)的内容区的边缘组成,也可能会建立格式化上下文(比如说 table container,flex container, grid container, 或者是 block container 自身)

    2. 如果 position 属性为 **absolute** ,包含块就是由它的最近的 position 的值不是 static (也就是值为fixed, absolute, relativesticky)的祖先元素的内边距区的边缘组成。

    3. 如果 position 属性是 fixed,在连续媒体的情况下(continuous media)包含块是 viewport ,在分页媒体(paged media)下的情况下包含块是分页区域(page area)。

    4. 如果 position 属性是 absolutefixed,包含块也可能是由满足以下条件的最近父级元素的内边距区的边缘组成的:

      1. transformperspective的值不是 none

      2. will-change 的值是 transformperspective

      3. filter的值不是 nonewill-change 的值是 filter(只在 Firefox 下生效).

      4. contain 的值是 paint (例如: contain: paint;)




    需要注意的是根元素(<html>)所在的包含块是一个被称为初始包含块的矩形。他的尺寸是视口 viewport (for continuous media) 或分页媒体 page media (for paged media).


    如果所有的父元素都没有显式地定义position属性,那么所有的父元素默认情况下 position 属性都是static。结果,绝对定位元素会被包含在初始包含块中;这个初始块容器有着和浏览器视口一样的尺寸,并且<html>元素也被包含在这个容器里面。简单来说,绝对定位元素会被放在<html>元素的外面,并且根据浏览器视口来定位。

    总结

    遇到奇奇怪怪的css问题不要慌~硬调不是好办法不如来我这瞅瞅~~ 万一就解决了呢✿✿ヽ(°▽°)ノ✿


    链接:https://juejin.cn/post/6963235293587750943

    收起阅读 »

    做了这个活动,感觉自己成了垂直排版css大师(文字竖排)

    前段时间协助完成作家的盘点活动,设计大大给的视觉稿如下图。最初拿到稿子时,以为是一个简单的常规活动,实际在完成这个活动的过程中有一点点小波折,自己也在这个过程中学习到了关于垂直排版的知识点,下面来跟大家描述做这个活动的心路历程,一起探索垂直排版的奥秘~文字竖排...
    继续阅读 »

    前段时间协助完成作家的盘点活动,设计大大给的视觉稿如下图。最初拿到稿子时,以为是一个简单的常规活动,实际在完成这个活动的过程中有一点点小波折,自己也在这个过程中学习到了关于垂直排版的知识点,下面来跟大家描述做这个活动的心路历程,一起探索垂直排版的奥秘~


    文字竖排🤔,立马想到使用 writing-mode,改变文字展示的行进方向


    大家都知道的writing-mode


    writing-mode 可以将文档流由水平方向调整为垂直方向


    以p标签内的一段文本为例,对其添加 writing-mode: vertical-rl,可以快速实现如下图所示的文字竖排效果


    p标签示例代码如下:

    <p class="text-vertical">爱交流,也爱独处</p>

    .text-vertical {
    writing-mode: vertical-rl;
    }

    一顿页面基础布局下来,可以看到页面效果和设计稿还是有差异😔,具体如下图的红色框线内;设计稿中红色框线内的数字是“直立向上”展示的


    感觉页面还原的展示也无伤大雅,于是拿着对比图找设计大大沟通;但是设计大大还是比较坚持视觉稿上的数字展示效果,以便更好传达活动内的关键信息(用户不需要旋转手机 or 侧头即可快速看清数据

    在纠结更好实现方案的时候,去请教了一下张老师,张老师提示可以使用 text-orientation:upright,即使是较差机型,可以优雅降级展示非竖向的效果,不影响活动


    向产品咨询了产品用户机型占比,较差机型的占比很低很低,而且产品也接受较低端机型降级的效果,完美💯

    部分人知道都text-orientation

    text-orientation: upright 可以将垂直排版的(设置了 writing-mode,且值不为 horizontal-tb)内容均直立向上展示


    在之前的示例基础上,添加 text-orientation: upright 即可实现将数字直立向上展示,面效果可以看下方图片中红色线框内数字


    p标签示例代码见下方:

    <p class="text-vertical">999篇日记,12356万字</p>
    .text-vertical {
    writing-mode: vertical-rl;
    text-orientation: upright;
    -webkit-text-orientation: upright;
    }

    调整完成后,活动大体完成,活动进入测试阶段;活动提测后,测试随口提了一句“这个日期看着有一点点不方便”



    虽然测试只是随口一说,但我就放在了心上,思考有没有可能优化一下这个日期显示呢?念念不忘,必有回响,偶然发现 text-combine-upright 的属性可以解决此类竖排横向合并

    较少人知道的text-combine-upright


    text-combine-upright,可以横向合并显示 2-4 字符,正好特别适合垂直排版中日期横向显示


    下图中红色线框内,就是添加 text-combine-upright: all 后日期横向合并的效果,“10” 被合并到一起展示,更利于读者快速获取文字信息


    示例代码见下方:

    <p class="text-vertical">
    <span class="upright-combine">10</span>月
    <span class="upright-combine">1</span>日时光日记上线
    </p>
    .text-vertical {
    writing-mode: vertical-rl;
    text-orientation: upright;
    }
    .upright-combine {
    /* for IE11+ */
    -ms-text-combine-horizontal: all;
    /* for Chrome/Firefox */
    text-combine-upright: all;
    /* for Safari */
    -webkit-text-combine: horizontal;
    }

    1)将待合并元素外包裹一层标签


    2)添加 text-combine-upright: all


    text-combine-upright 属性支持关键字值和数字值,考虑到数字值的兼容性不佳,此处主要是用使用关键字all实现的


    3)兼容处理,对 IE 浏览器和 Safari 浏览器做兼容处理(支持 IE11+),说明及代码见下方:


    IE 浏览器使用的是 -ms-text-combine-horizontal 属性,与 text-combine-upright 属性一致 Safari 浏览器使用 -webkit-text-combine 属性,仅支持 none 和 horizontal 属性值


    项目中实际代码片段为下方所示,可以兼容移动端项目,日期展示优化完成!

    .upright-combine {
    /* for IE11+ */
    -ms-text-combine-horizontal: all;
    text-combine-upright: all;
    /* forSafari */
    -webkit-text-combine: horizontal;
    }

    text-combine-upright 使用注意事项:



    • 只用于 2-4 字符的横向合并,例如日期、年份等

    • 由于数字值的兼容性不佳,需要将待横向合并的内容包裹标签


    至此,该活动顺利完成并上线了,在完成改活动的过程并非一蹴而就。关于数字垂直排版的实现方案,中间也实践过一些其他方案,可能会对大家未来实现类似场景有参考价值~

    数字垂直排版的其他方案

    最初在考虑实现数字垂直排版的时候,实践过其他方案,下面跟大家分享一下~


    1.JS分割大法


    JS 将所有的文本信息均切割为单个标签,将单个文本内容都作为 block 元素,此时无需设置 writing-mode,直接设置外段落文本的最大宽度为 1em,即可达到内容竖向 + 数字直立向上效果


    核心代码如下所示:

    function text2html(element) {
    var treeWalker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
    var node;
    let list = []
    while ((node = treeWalker.nextNode())) {
    list.push(node)
    }
    // 纯文本节点数组遍历
    list.forEach(function(node) {
    // 纯文本节点切割,替换为span标签
    var replaceArr = node.textContent.split('').map(function(str) {
    var newSpan = document.createElement('span');
    newSpan.textContent = str;
    return newSpan;
    });
    node.replaceWith.apply(node, replaceArr);
    })
    }

    JS 分割大法,将所有文本内容拆分成单个标签,方案简单粗暴,可能会提高样式布局难度,有潜在风险,故最终未采用该方法,但它较适合对局部个别文案处理、兜底展示或者低端机型处理

    2.半角数字转全角

    数字为全角时,此时垂直展示的时候,它的展示效果和中文类似,天然“直立向上” ,因而可以将半角数字转成全角数字

    数字半角转全角的示例代码如下:

    // 数字半转全 0-9 对应 半角对应48-57,全角对应65296-65305
    function ToDBC(txtstring) {
    var newNumber = "";
    for(var i = 0; i < txtstring.length; i++) {
    if(txtstring.charCodeAt(i) <= 57 && txtstring.charCodeAt(i) >= 48) {
    newNumber = newNumber + String.fromCharCode(txtstring.charCodeAt(i)+65248);
    } else {
    newNumber = newNumber + txtstring.substring(i, i+1);
    }
    }
    return newNumber;
    }

    数字半角转全角的方法看似非常完美,但是后续有需要额外考虑哪些不转全角,并且涉及到一些数字计算判断时,还需要把全角转半角(全角数字无法直接参与逻辑运算),展示再转全角,处理起来较为繁琐,故最终没选择此技术方案

    数字“直立向上”方案小结



    • CSS的 text-orientation

    • 暴力拆解 DOM

    • 调整数字为全角


    关于数字“直立向上”的实现,如果大家有其他好的思考角度或者解决方案,欢迎分享交流~


    最终的归纳总结



    • 竖向展示排版通过 writing-mode 可快速实现

    • 对于部分数字需要“直立向上”时,采用 text-orientation:upright 方法

    • 日期类字符(2-4字符)可以通过 text-combine-upright 属性横向合并优化展示



    链接:https://juejin.cn/post/6966449320744714277
    收起阅读 »

    6分钟实现CSS炫光倒影按钮

    话不多,先看效果: 回归老本行,继续分享简单有趣的CSS创意特效,放松放松心情~实现过程(完整源码在最后):1 老样子,定义基本样式:*{ margin: 0; padding: 0; box...
    继续阅读 »

    话不多,先看效果:

     回归老本行,继续分享简单有趣的CSS创意特效,放松放松心情~


    实现过程(完整源码在最后):

    1 老样子,定义基本样式:

    *{
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    font-family: 'fangsong';
    }
    body{
    height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: rgb(0, 0, 0);
    }

    font-family: 'fangsong'; 仿宋字体。 display: flex; align-items: center; justify-content: center; flex布局,让按钮在屏幕居中。

    2.定义基本标签:

            
    aurora






    aurora






    aurora




    3个a标签就对应3个按钮,每个按钮里4个span就是环绕按钮的4条边。 且都有个公共的选择器 .item 和 只属于自己的选择器。

    3.定义每个按钮的基本样式:

        .item{
    position: relative;
    margin: 50px;
    width: 300px;
    height: 80px;
    text-align: center;
    line-height: 80px;
    text-transform: uppercase;
    text-decoration: none;
    font-size: 35px;
    letter-spacing: 5px;
    color: aqua;
    overflow: hidden;
    -webkit-box-reflect: below 1px linear-gradient( transparent,rgba(6, 133, 133,0.3));
    }
    text-align: center;文字对齐方式。
    line-height: 80px; 字行高。
    text-transform: uppercase; 字母为大写。
    text-decoration: none; 去掉a标签默认下划线。
    letter-spacing: 5px; 每个字符间的距离。
    overflow: hidden;溢出隐藏。
    -webkit-box-reflect: below 1px linear-gradient( transparent,rgba(6, 133, 133,0.3)); 这个属性能实现倒影效果。

    4. 鼠标经过按钮样式改变:

    .item:hover{
    background-color: aqua;
    box-shadow:0 0 5px aqua,
    0 0 75px aqua,
    0 0 155px aqua;
    color: black;
    }

    box-shadow:0 0 5px aqua, 0 0 75px aqua, 0 0 155px aqua; 阴影,写多行可以叠加更亮。

    5.设置环绕按钮的4根线上面那条的样式:

        .item span:nth-of-type(1){
    position: absolute;
    left: -100%;
    width: 100%;
    height: 3px;
    background-image: linear-gradient(to left,aqua ,transparent);
    animation: shang 1s linear infinite;
    }
    @keyframes shang{
    0%{
    left:-100%;
    }
    50%,100%{
    left:100%;
    }
    }

    position: absolute;
    left: -100%; 定位在对应位置。
    background-image: linear-gradient(to left,aqua ,transparent); 线性渐变颜色。
    animation: shang 1s linear infinite; 动画属性,让它动起来。


    5.以此类推,设置环绕按钮的其它3根样式:

    .item span:nth-of-type(2) {
    position: absolute;
    top: -100%;
    right: 0;
    width: 3px;
    height: 100%;
    background-image: linear-gradient(to top, aqua, transparent);
    animation: you 1s linear infinite;
    animation-delay: 0.25s;
    }
    @keyframes you {
    0% {
    top: -100%;
    }
    50%,
    100% {
    top: 100%;
    }
    }
    .item span:nth-of-type(3) {
    position: absolute;
    right: -100%;
    bottom: 0;
    width: 100%;
    height: 3px;
    background-image: linear-gradient(to right, aqua, transparent);
    animation: xia 1s linear infinite;
    animation-delay: 0.5s;
    }
    @keyframes xia {
    0% {
    right: -100%;
    }
    50%,
    100% {
    right: 100%;
    }
    }
    .item span:nth-of-type(4) {
    position: absolute;
    bottom: -100%;
    left: 0;
    width: 3px;
    height: 100%;
    background-image: linear-gradient(to bottom, aqua, transparent);
    animation: zuo 1s linear infinite;
    animation-delay: 0.75s;
    }
    @keyframes zuo {
    0% {
    bottom: -100%;
    }
    50%,
    100% {
    bottom: 100%;
    }
    }

    animation-delay: 0.75s; 动画延迟执行。每条线对应延迟一段时间,形成时间差,形成环绕效果。

    6.给第一,第三个按钮设置其它颜色:

        .item1{
    filter: hue-rotate(100deg);
    }
    .item3{
    filter: hue-rotate(250deg);
    }

    完整代码:

    <!DOCTYPE html>
    <html lang="zh-CN">

    <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
    * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    font-family: 'fangsong';
    }

    body {
    height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: rgb(0, 0, 0);
    }

    .item {
    position: relative;
    margin: 50px;
    width: 300px;
    height: 80px;
    text-align: center;
    line-height: 80px;
    text-transform: uppercase;
    text-decoration: none;
    font-size: 35px;
    letter-spacing: 5px;
    color: aqua;
    overflow: hidden;
    -webkit-box-reflect: below 1px linear-gradient(transparent, rgba(6, 133, 133, 0.3));
    }

    .item:hover {
    background-color: aqua;
    box-shadow: 0 0 5px aqua,
    0 0 75px aqua,
    0 0 155px aqua;
    color: black;
    }

    .item span:nth-of-type(1) {
    position: absolute;
    left: -100%;
    width: 100%;
    height: 3px;
    background-image: linear-gradient(to left, aqua, transparent);
    animation: shang 1s linear infinite;
    }

    @keyframes shang {
    0% {
    left: -100%;
    }

    50%,
    100% {
    left: 100%;
    }
    }

    .item span:nth-of-type(2) {
    position: absolute;
    top: -100%;
    right: 0;
    width: 3px;
    height: 100%;
    background-image: linear-gradient(to top, aqua, transparent);
    animation: you 1s linear infinite;
    animation-delay: 0.25s;
    }

    @keyframes you {
    0% {
    top: -100%;
    }

    50%,
    100% {
    top: 100%;
    }
    }

    .item span:nth-of-type(3) {
    position: absolute;
    right: -100%;
    bottom: 0;
    width: 100%;
    height: 3px;
    background-image: linear-gradient(to right, aqua, transparent);
    animation: xia 1s linear infinite;
    animation-delay: 0.5s;
    }

    @keyframes xia {
    0% {
    right: -100%;
    }

    50%,
    100% {
    right: 100%;
    }
    }

    .item span:nth-of-type(4) {
    position: absolute;
    bottom: -100%;
    left: 0;
    width: 3px;
    height: 100%;
    background-image: linear-gradient(to bottom, aqua, transparent);
    animation: zuo 1s linear infinite;
    animation-delay: 0.75s;
    }

    @keyframes zuo {
    0% {
    bottom: -100%;
    }

    50%,
    100% {
    bottom: 100%;
    }
    }

    .item1 {
    filter: hue-rotate(100deg);
    }

    .item3 {
    filter: hue-rotate(250deg);
    }
    </style>
    </head>

    <body>

    <a href="#" class="item item1">
    aurora
    <span></span>
    <span></span>
    <span></span>
    <span></span>
    </a>
    <a href="#" class="item item2">
    aurora
    <span></span>
    <span></span>
    <span></span>
    <span></span>
    </a>
    <a href="#" class="item item3">
    aurora
    <span></span>
    <span></span>
    <span></span>
    <span></span>
    </a>

    </body>

    </html>


    原文:https://juejin.cn/post/6966482130020859912

    收起阅读 »

    为了能够摸鱼,我走上了歧路

    前言 每天都是重复的工作,这样可不行,已经严重影响我的日常摸鱼,为了减少自己日常的开发时间,我决定走一条歧路,铤而走险,将项目中的各种手动埋点统计替换成自动化埋点。以后再也不用担心没时间摸鱼了~ 作为Android届开发的一员,今天我决定将摸鱼方案分享给大家,...
    继续阅读 »

    前言


    每天都是重复的工作,这样可不行,已经严重影响我的日常摸鱼,为了减少自己日常的开发时间,我决定走一条歧路,铤而走险,将项目中的各种手动埋点统计替换成自动化埋点。以后再也不用担心没时间摸鱼了~


    作为Android届开发的一员,今天我决定将摸鱼方案分享给大家,希望更多的广大群众能够的加入到摸鱼的行列中~



    1. 声明打点的接口方法
    interface StatisticService {

    @Scan(ProxyActivity.PAGE_NAME)
    fun buttonScan(@Content(StatisticTrack.Parameter.NAME) name: String)

    @Click(ProxyActivity.PAGE_NAME)
    fun buttonClick(@Content(StatisticTrack.Parameter.NAME) name: String, @Content(StatisticTrack.Parameter.TIME) clickTime: Long)

    @Scan(ProxyActivity.PAGE_NAME)
    fun textScan(@Content(StatisticTrack.Parameter.NAME) name: String)

    @Click(ProxyActivity.PAGE_NAME)
    fun textClick(@Content(StatisticTrack.Parameter.NAME) name: String, @Content(StatisticTrack.Parameter.TIME) clickTime: Long)
    }




    1. 通过动态代理获取StatisticService接口引用
    private val mStatisticService = Statistic.instance.create(StatisticService::class.java)




    1. 在合适的埋点位置进行埋点统计,例如Click埋点
    2. fun onClick(view: View) {
      if (view.id == R.id.button) {
      mStatisticService.buttonClick(BUTTON, System.currentTimeMillis() / 1000)
      } else if (view.id == R.id.text) {
      mStatisticService.textClick(TEXT, System.currentTimeMillis() / 1000)
      }
      }



    其中2、3步骤都是在对应埋点的类中使用,这里对应的是ProxyActivity

    class ProxyActivity : AppCompatActivity() {

    // 步骤2
    private val mStatisticService = Statistic.instance.create(StatisticService::class.java)

    companion object {
    private const val BUTTON = "statistic_button"
    private const val TEXT = "statistic_text"
    const val PAGE_NAME = "ProxyActivity"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
    //...
    title = extraData.title

    // 步骤3 => 曝光点
    mStatisticService.buttonScan(BUTTON)
    mStatisticService.textScan(TEXT)
    }

    private fun getExtraData(): MainModel =
    intent?.extras?.getParcelable(ActivityUtils.EXTRA_DATA)
    ?: throw NullPointerException("intent or extras is null")

    // 步骤3 => 点击点
    fun onClick(view: View) {
    if (view.id == R.id.button) {
    mStatisticService.buttonClick(BUTTON, System.currentTimeMillis() / 1000)
    } else if (view.id == R.id.text) {
    mStatisticService.textClick(TEXT, System.currentTimeMillis() / 1000)
    }
    }
    }



    步骤1是创建新的类,不在代码注入的范围之内。自动生成类可以使用注解+process+JavaPoet来实现。类似于ButterKnifeDagger2Room等。之前我也有写过相关的demo与文章。由于不在本篇文章的范围之内,感兴趣的可以自行去了解。


    这里我们需要做的是:需要在ProxyActiviy中将2、3步骤的代码转成自动注入。


    自动注入就是在现有的类中自动加入我们预期的代码,不需要我们额外的进行编写。


    既然已经知道了需要注入的代码,那么接下的问题就是什么时候进行注入这些代码。


    这就涉及到Android构建与打包的流程,Android使用Gradle进行构建与打包,


    image.png


    在打包的过程中将源文件转化成.class文件,然后再将.class文件转成Android能识别的.dex文件,最终将所有的.dex文件组合成一个.apk文件,提供用户下载与安装。


    而在将源文件转化成.class文件之后,Google提供了一种Transform机制,允许我们在打包之前对.class文件进行修改。


    这个修改时机就是我们代码自动注入的时机。


    transform是由gradle提供,在我们日常的构建过程中也会看到系统自身的transform身影,gradle由各种task组成,transform就穿插在这些task中。


    图中高亮的部分就是本次自定义的TraceTransform, 它会在.class转化成.dex之前进行执行,目的就是修改目标.class文件内容。


    Transform的实现需要结合Gradle Plugin一起使用。所以接下来我们需要创建一个Plugin


    创建Plugin


    appbuild.gradle中,我们能够看到以下类似的插件引用方式

    apply plugin: 'com.android.application'
    apply plugin: 'kotlin-android'
    apply plugin: 'kotlin-android-extensions'
    apply plugin: 'kotlin-kapt'
    apply plugin: "androidx.navigation.safeargs.kotlin"
    apply plugin: 'trace_plugin'



    这里的插件包括系统自带、第三方的与自定义的。其中trace_plugin就是本次自定义的插件。为了能够让项目使用自定义的插件,Gradle提供了三种打包插件的方式



    1. Build Script: 将插件的源代码直接包含在构建脚本中。这样做的好处是,无需执行任何操作即可自动编译插件并将其包含在构建脚本的类路径中。但缺点是它在构建脚本之外不可见,常用在脚本自动构建中。

    2. buildSrc projectgradle会自动识别buildSrc目录,所以可以将plugin放到buildSrc目录中,这样其它的构建脚本就能自动识别这个plugin, 多用于自身项目,对外不共享。

    3. Standalone project: 创建一个独立的plugin项目,通过对外发布Jar与外部共享使用。


    这里使用第三种方式来创建Plugin。所以创建完之后的目录结构大概是这样的


    为了让别的项目能够引用这个Plugin,我们需要对外声明,可以发布到maven中,也可以本地声明,为了简便这里使用本地声明。

    apply plugin: 'java-gradle-plugin'

    dependencies {
    implementation gradleApi()
    implementation localGroovy()
    }

    gradlePlugin {
    plugins {
    version {
    // 在 app 模块需要通过 id 引用这个插件
    id = 'trace_plugin'
    // 实现这个插件的类的路径
    implementationClass = 'com.rousetime.trace_plugin.TracePlugin'
    }
    }
    }



    Pluginidtrace_plugin,实现入口为com.rousetime.trace_plugin.TracePlugin


    声明完之后,就可以直接在项目的根目录下的build.gradle中引入该id

    plugins {
    id "trace_plugin" apply false
    }


    为了能在app项目中apply这个plugin,还需要创建一个META-INF.gradle-plugins目录,对应的位置如下


    注意这里的trace_plugin.properties文件名非常重要,前面的trace_plugin就代表你在build.gradleapply的插件名称。


    文件中的内容很简单,只有一行,对应的就是TracePlugin的实现入口

    implementation-class=com.rousetime.trace_plugin.TracePlugin

    上面都准备就绪之后,就可以在build.gradle进行apply plugin

    apply plugin: 'trace_plugin'


    这个时候我们自定义的plugin就引入到项目中了。


    再回到刚刚的Plugin入口TracePlugin,来看下它的具体实现

    class TracePlugin : Plugin {

    override fun apply(target: Project) {
    if (target.plugins.hasPlugin(AppPlugin::class.java)) {
    val appExtension = target.extensions.getByType(AppExtension::class.java)
    appExtension.registerTransform(TraceTransform())
    }
    val methodVisitorConfig = target.extensions.create("methodVisitor", MethodVisitorConfig::class.java)
    LocalConfig.methodVisitorConfig = methodVisitorConfig
    target.afterEvaluate {
    }
    }

    }



    只有一个方法apply,在该方法中我们打印一行文本,然后重新构建项目,在build输出窗口就能看到这行文本

    ....
    > Configure project :app
    Trace Plugin start to apply
    mehtodVisitorConfig

    Deprecated Gradle features were used in this build, making it incompatible with Gradle 6.0.
    Use '--warning-mode all' to show the individual deprecation warnings.
    ...



    到这里我们自定义的plugin已经创建成功,并且已经集成到我们的项目中。


    第一步已经完成。下面进入第二步。


    实现Transform


    TracePluginapply方法中,对项目的appExtension注册了一个TraceTransform。重点来了,这个TraceTransform就是我们在gradle构建的过程中插入的Transform,也就是注入代码的入口。来看下它的具体实现

    class TraceTransform : Transform() {

    override fun getName(): String = this::class.java.simpleName

    override fun getInputTypes(): MutableSet = TransformManager.CONTENT_JARS

    override fun isIncremental(): Boolean = true

    override fun getScopes(): MutableSet = TransformManager.SCOPE_FULL_PROJECT

    override fun transform(transformInvocation: TransformInvocation?) {
    TransformProxy(transformInvocation, object : TransformProcess {
    override fun process(entryName: String, sourceClassByte: ByteArray): ByteArray? {
    // use ams to inject
    return if (ClassUtils.checkClassName(entryName)) {
    TraceInjectDelegate().inject(sourceClassByte)
    } else {
    null
    }
    }
    }).apply {
    transform()
    }
    }
    }



    代码很简单,只需要实现几个特定的方法。



    1. getName: Transform对外显示的名称

    2. getInputTypes: 扫描的文件类型,CONENT_JARS代表CLASSESRESOURCES

    3. isIncremental: 是否开启增量,开启后会提高构建速度,对应的需要手动处理增量的逻辑

    4. getScopes: 扫描作用范围,SCOPE_FULL_PROJECT代表整个项目

    5. transform: 需要转换的逻辑都在这里处理


    transform是我们接下来.class文件的入口,这个方法有个参数TransformInvocation,该参数提供了上面定义范围内扫描到的所用jar文件与directory文件。


    transform中我们主要做的就是在这些jardirectory中解析出.class文件,这是找到目标.class的第一步。只有解析出了所有的.class文件,我们才能进一步过滤出我们需要注入代码的.class文件。


    transform的工作流程是:解析.class文件,然后我们过滤出需要处理的.class文件,写入对应的逻辑,然后再将处理过的.class文件重新拷贝到之前的jar或者directory中。


    通过这种解析、处理与拷贝的方式,实现偷天换日的效果。


    既然有一套固定的流程,那么自然有对应的一套固定是实现。在这三个步骤中,真正需要实现的是处理逻辑,不同的项目有不同的处理逻辑,


    对于解析与拷贝操作,已经有相对完整的一套通用实现方案。如果你的项目中有多个这种类型的Transform,就可以将其抽离出来单个module,增加复用性。


    解析与拷贝


    下面我们来看一下它的核心实现步骤。

    fun transform() {
    if (!isIncremental) {
    // 不是增量编译,将之前的输出目录中的内容全部删除
    outputProvider?.deleteAll()
    }
    inputs?.forEach {
    // jar
    it.jarInputs.forEach { jarInput ->
    transformJar(jarInput)
    }
    // directory
    it.directoryInputs.forEach { directoryInput ->
    transformDirectory(directoryInput)
    }
    }
    executor?.invokeAll(tasks)
    }



    transform方法主要做的就是分别遍历jardirectory中的文件。在这两大种类中分别解析出.class文件。


    例如jar的解析transformJar


    如果是增量编译,就分别处理增量的不同操作,主要的是ADDEDCHANGED操作。这个处理逻辑与非增量编译的时候一样,都是去遍历jar,从中解析出对应的.class文件。


    遍历的核心代码如下

    while (enumeration.hasMoreElements()) {
    val jarEntry = enumeration.nextElement()
    val inputStream = originalFile.getInputStream(jarEntry)

    val entryName = jarEntry.name
    // 构建zipEntry
    val zipEntry = ZipEntry(entryName)
    jarOutputStream.putNextEntry(zipEntry)

    var modifyClassByte: ByteArray? = null
    val sourceClassByte = IOUtils.toByteArray(inputStream)

    if (entryName.endsWith(".class")) {
    modifyClassByte = transformProcess.process(entryName, sourceClassByte)
    }

    if (modifyClassByte == null) {
    jarOutputStream.write(sourceClassByte)
    } else {
    jarOutputStream.write(modifyClassByte)
    }
    inputStream.close()
    jarOutputStream.closeEntry()
    }



    如果entryName的后缀是.class说明当前是.class文件,我们需要单独拿出来进行后续的处理。


    后续的处理逻辑交给了transformProcess.process。具体处理先放一放。


    处理完之后,再将处理后的字节码拷贝保存到之前的jar中。


    对应的directory也是类似 同样是过滤出.class文件,然后交给process方法进行统一处理。最后将处理完的字节码拷贝保存到原路径中。


    以上就是Transform的解析与拷贝的核心处理。


    处理


    上面提到.class的处理都转交给process方法,这个方法的具体实现在TraceTransformtransform方法中

    class TraceAsmInject : Inject {

    override fun modifyClassByte(byteArray: ByteArray): ByteArray {
    val classWriter = ClassWriter(ClassWriter.COMPUTE_MAXS)
    val classFilterVisitor = ClassFilterVisitor(classWriter)
    val classReader = ClassReader(byteArray)
    classReader.accept(classFilterVisitor, ClassReader.EXPAND_FRAMES)
    return classWriter.toByteArray()
    }

    }



    process中使用TraceInjectDelegateinject来处理过滤出来的字节码。最终的处理会来到modifyClassByte方法。



    这里的ClassWriterClassFilterVisitorClassReader都是ASM的内容,也是我们接下来实现自动注入代码的重点。


    ASM


    ASM是操作Java字节码的一个工具。


    其实操作字节码的除了ASM还有javassist,但个人觉得ASM更方便,因为它有一系列的辅助工具,能更好的帮助我们实现代码的注入。


    在上面我们已经得到了.class的字节码文件。现在我们需要做的就是扫描整个字节码文件,判断是否是我们需要注入的文件。


    这里我将这些逻辑封装到了ClassFilterVisitor文件中。


    ASM为我们提供了ClassVisitorMethodVisitorFieldVisitorAPI。每当ASM扫描类的字节码时,都会调用它的visitvisitFieldvisitMethodvisitAnnotation等方法。


    有了这些方法,我们就可以判断并处理我们需要的字节码文件。

    class ClassFilterVisitor(cv: ClassVisitor?) : ClassVisitor(Opcodes.ASM5, cv) {

    override fun visit(version: Int, access: Int, name: String?, signature: String?, superName: String?, interfaces: Array?) {
    super.visit(version, access, name, signature, superName, interfaces)
    // 扫描当前类的信息
    }

    override fun visitMethod(access: Int, name: String?, desc: String?, signature: String?, exceptions: Array?): MethodVisitor {
    // 扫描类中的方法
    }


    override fun visitField(access: Int, name: String?, desc: String?, signature: String?, value: Any?): FieldVisitor {
    // 扫描类中的字段
    }

    }



    这是几个主要的方法,也是接下来我们需要重点用到的方法。


    首先我们来看个简单的,这个明白了其它的都是一样的。

    fun bindData(value: MainModel, position: Int) {
    itemView.content.apply {
    text = value.content
    setOnClickListener {
    // 自动注入这行代码
    LogUtils.d("inject success.")
    if (position == 0) {
    requestPermission(context, value)
    } else {
    navigationPage(context, value)
    }
    }
    }
    }



    假设我们需要在onClickListener中注入LogUtils.d这个行代码,本质就是在点击的时候输出一行日志。


    首先我们需要明白,setOnClickListener本质是实现了一个OnClickListener接口的匿名内部类。


    所以可以在扫描类的时候判断是否实现了OnClickListener这个接口,如果实现了,我们再去匹配它的onClick方法,并且在它的onClick方法中进行注入代码。


    而类的扫描与方法扫描分别可以使用visitvisitMetho


    visit方法中,我们保存当前类实现的接口;在visitMethod中再对当前接口进行判断,看它是否有onClick方法。



    namedesc分别为onClick方法的方法名称与方法参数描述。这是字节码匹配方法的一种规范。



    如果有的话,说明是我们需要插入的方法,这个时候返回AdviceAdapter。它是ASM提供的便捷针对方法注入的类。我们重写它的onMethodEnter方法。代表我们将在方法的开头注入代码。


    onMethodEnter方法中的代码就是LogUtils.dASM注入实现。你可能会说这个是什么,完全看不懂,更别说写字节码注入了。


    别急,下面就是ASM的方便之处,我们只需在Android Studio中下载ASM Bytecode Viewer Support Kotlin插件。


    该插件可以帮助我们查看kotlin字节码,只需右键弹窗中选择ASM Bytecode Viewer。稍后就会弹出转化后的字节码弹窗。


    在弹窗中找到需要注入的代码,具体就是下面这几行

    methodVisitor.visitFieldInsn(GETSTATIC, "com/idisfkj/androidapianalysis/utils/LogUtils", "INSTANCE", "Lcom/idisfkj/androidapianalysis/utils/LogUtils;");
    methodVisitor.visitLdcInsn("inject success.");
    methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "com/idisfkj/androidapianalysis/utils/LogUtils", "d", "(Ljava/lang/String;)V", false);



    这就是LogUtils.d的注入代码,直接copy到上面提到的onMethodEnter方法中。这样注入的代码就已经完成。


    如果你想查看是否注入成功,除了运行项目,查看效果之外,还可以直接查看注入的源码。


    在项目的build/intermediates/transforms目录下,找到自定义的TraceTransform,再找到对应的注入文件,就可以查看注入源码。


    其实到这来核心内容基本已经结束了,不管是注入什么代码都可以通过这种方法来获取注入的ASM的代码,不同的只是注入的时机判断。


    有了上面的基础,我们来实现开头的自动埋点。


    实现


    为了让自动化埋点能够灵活的传递打点数据,我们使用注解的方式来传递具体的埋点数据与类型。



    1. TrackClickData: 点击的数据

    2. TrackScanData: 曝光的数据

    3. TrackScan: 曝光点

    4. TrackClick: 点击点


    有了这些注解,剩下我们要做的就很简单了


    使用TrackClickDataTrackScanData声明打点的数据;使用TrackScanTrackClick声明打点的类型与自动化插入代码的入口方法。


    我们再回到注入代码的类ClassFilterVisitor,来实现具体的埋点代码的注入。


    在这里我们需要做的是解析声明的注解,拿到打点的数据,并且声明的TrackScanTrackClick方法中插入埋点的具体代码。

    override fun visit(version: Int, access: Int, name: String?, signature: String?, superName: String?, interfaces: Array?) {
    super.visit(version, access, name, signature, superName, interfaces)
    mInterface = interfaces
    mClassName = name
    }



    通过visit方法来扫描具体的类文件,在这里保存当前扫描的类的信息,为之后注入代码做准备

    override fun visitField(access: Int, name: String?, desc: String?, signature: String?, value: Any?): FieldVisitor {
    val filterVisitor = super.visitField(access, name, desc, signature, value)
    return object : FieldVisitor(Opcodes.ASM5, filterVisitor) {
    override fun visitAnnotation(annotationDesc: String?, visible: Boolean): AnnotationVisitor {
    if (annotationDesc == TRACK_CLICK_DATA_DESC) { // TrackClickData 注解
    mTrackDataName = name
    mTrackDataValue = value
    mTrackDataDesc = desc
    createFiled()
    } else if (annotationDesc == TRACK_SCAN_DATA_DESC) { // TrackScanData注解
    mTrackScanDataName = name
    mTrackScanDataDesc = desc
    createFiled()
    }
    return super.visitAnnotation(annotationDesc, visible)
    }
    }
    }



    visitFiled方法用来扫描类文件中声明的字段。在该方法中,我们返回并实现FieldVisitor,并重新它的visitAnnotation方法,目的是找到之前TrackClickDataTrackScanData声明的埋点字段。对应的就是mTrackModelmTrackScanData


    主要包括字段名称name与字段的描述desc,为我们之后注入埋点数据做准备。


    另外一旦匹配到埋点数据的注解,说明该类中需要进行自动化埋点,所以还需要自动创建StatisticService。这是打点的接口方法,具体打点的都是通过StatisticService来实现。


    visitField中,通过createFiled方法来创建StatisticService类型的字段

    private fun createFiled() {
    if (!mFieldPresent) {
    mFieldPresent = true
    // 注入:statisticService 字段
    val fieldVisitor = cv.visitField(ACC_PRIVATE or ACC_FINAL, statisticServiceField.name, statisticServiceField.desc, null, null)
    fieldVisitor.visitEnd()
    }
    }



    其中statisticServiceField是封装好的StatisticService字段信息。

    companion object {
    const val OWNER = "com/idisfkj/androidapianalysis/proxy/StatisticService"
    const val DESC = "Lcom/idisfkj/androidapianalysis/proxy/StatisticService;"

    val INSTANCE = StatisticService()
    }

    val statisticService = FieldConfig(
    Opcodes.PUTFIELD,
    "",
    "mStatisticService",
    DESC
    )



    创建的字段名为mStatisticService,它的类型是StatisticService


    到这里我们已经拿到了埋点的数据字段,并创建了埋点的调用字段mStatisticService;接下来要做的就是注入埋点代码。


    核心注入代码在visitMethod方法中,该方法用来扫描类中的方法。所以类中声明的方法都会在这个方法中进行扫描回调。


    visitMethod中,我们找到目标的埋点方法,即之前声明的方法注解TrackScanTrackClick


    返回并实现AdviceAdapter,重写它的visitAnnotation方法。


    该方法会自动扫描方法的注解,所以可以通过该方法来保存当前方法的注解。


    然后在onMethodExit中,即方法的开头处进行注入代码。


    在该方法中主要做三件事



    1. 向默认构造方法中,实例化statisticService

    2. 注入TrackClick 点击

    3. 注入TrackScan 曝光


    具体的ASM注入代码可以通过之前说的SM Bytecode Viewer Support Kotlin插件获取。


    有了上面的实现,再来运行运行主项目,你就会发现埋点代码已经自动注入成功。


    我们反编译一下.class文件,来看下注入后的java代码


    StatisticService初始化

    public ProxyActivity() {
    boolean var2 = false;
    List var3 = (List)(new ArrayList());
    this.mTrackScanData = var3;
    // 以下是注入代码
    this.mStatisticService = (StatisticService)Statistic.Companion.getInstance().create(StatisticService.class);
    }



    曝光埋点

    @TrackScan
    public final void onScan() {
    this.mTrackScanData.add(new TrackModel("statistic_button", 0L, 2, (DefaultConstructorMarker)null));
    this.mTrackScanData.add(new TrackModel("statistic_text", 0L, 2, (DefaultConstructorMarker)null));
    // 以下是注入代码
    LogUtils.INSTANCE.d("inject track scan success.");
    Iterator var2 = this.mTrackScanData.iterator();

    while(var2.hasNext()) {
    TrackModel var1 = (TrackModel)var2.next();
    this.mStatisticService.trackScan(var1.getName());
    }

    }



    点击埋点

    @TrackClick
    public final void onClick(@NotNull View view) {
    Intrinsics.checkParameterIsNotNull(view, "view");
    this.mTrackModel.setTime(System.currentTimeMillis() / (long)1000);
    this.mTrackModel.setName(view.getId() == 2131230792 ? "statistic_button" : "statistic_text");
    // 以下是注入代码
    LogUtils.INSTANCE.d("inject track click success.");
    this.mStatisticService.trackClick(this.mTrackModel.getName(), this.mTrackModel.getTime());
    }



    以上自动化埋点代码就已经完成了。


    简单总结一下,所用到的技术有



    1. gradle plugin插件的自定义

    2. gradle transform提供编译中字节码的修改入口

    3. asm提供代码的注入实现


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

    因为这几个 TypeScript 代码的坏习惯,同事被罚了 500 块

    近几年 TypeScript 和 JavaScript 一直在稳步发展。我们在过去写代码时养成了一些习惯,而有些习惯却没有什么意义。以下是我们都应该改正的 10 个坏习惯。1.不使用 strict 模式这种习惯看起来是什么样的没有用严格模式编写 tsconfi...
    继续阅读 »

    近几年 TypeScript 和 JavaScript 一直在稳步发展。我们在过去写代码时养成了一些习惯,而有些习惯却没有什么意义。以下是我们都应该改正的 10 个坏习惯。

    1.不使用 strict 模式

    • 这种习惯看起来是什么样的

    没有用严格模式编写 tsconfig.json。

    {
    "compilerOptions": {
    "target": "ES2015",
    "module": "commonjs"
    }
    }
    • 应该怎样

    只需启用 strict 模式即可:

    {
    "compilerOptions": {
    "target": "ES2015",
    "module": "commonjs",
    "strict": true
    }
    }


    • 为什么会有这种坏习惯

    在现有代码库中引入更严格的规则需要花费时间。

    • 为什么不该这样做

    更严格的规则使将来维护代码时更加容易,使你节省大量的时间。

    2. 用 ||定义默认值

    • 这种习惯看起来是什么样的

    使用旧的 ||处理后备的默认值:

    function createBlogPost (text: string, author: string, date?: Date) {
    return {
    text: text,
    author: author,
    date: date || new Date()
    }
    }


    • 应该怎样


    使用新的 ??运算符,或者在参数重定义默认值。


    function createBlogPost (text: string, author: string, date: Date = new Date())
    return {
    text: text,
    author: author,
    date: date
    }
    }


    • 为什么会有这种坏习惯


    ??运算符是去年才引入的,当在长函数中使用值时,可能很难将其设置为参数默认值。



    • 为什么不该这样做


    ??与 ||不同,??仅针对 null 或 undefined,并不适用于所有虚值。

    3. 随意使用 any 类型

    • 这种习惯看起来是什么样的

    当你不确定结构时,可以用 any 类型。

    async function loadProducts(): Promise<Product[]> {
    const response = await fetch('https://api.mysite.com/products')
    const products: any = await response.json()
    return products
    }
    • 应该怎样


    把你代码中任何一个使用 any 的地方都改为 unknown


    async function loadProducts(): Promise<Product[]> {
    const response = await fetch('https://api.mysite.com/products')
    const products: unknown = await response.json()
    return products as Product[]
    }


    • 为什么会有这种坏习惯


    any 是很方便的,因为它基本上禁用了所有的类型检查。通常,甚至在官方提供的类型中都使用了 any。例如,TypeScript 团队将上面例子中的 response.json()的类型设置为 Promise。



    • 为什么不该这样做


    它基本上禁用所有类型检查。任何通过 any 进来的东西将完全放弃所有类型检查。这将会使错误很难被捕获到。


    4. val as SomeType



    • 这种习惯看起来是什么样的


    强行告诉编译器无法推断的类型。


    async function loadProducts(): Promise<Product[]> {
    const response = await fetch('https://api.mysite.com/products')
    const products: unknown = await response.json()
    return products as Product[]
    }


    • 应该怎样


    这正是 Type Guard 的用武之地。


    function isArrayOfProducts (obj: unknown): obj is Product[] {
    return Array.isArray(obj) && obj.every(isProduct)
    }

    function isProduct (obj: unknown): obj is Product {
    return obj != null
    && typeof (obj as Product).id === 'string'
    }

    async function loadProducts(): Promise<Product[]> {
    const response = await fetch('https://api.mysite.com/products')
    const products: unknown = await response.json()
    if (!isArrayOfProducts(products)) {
    throw new TypeError('Received malformed products API response')
    }
    return products
    }


    • 为什么会有这种坏习惯


    从 JavaScript 转到 TypeScript 时,现有的代码库通常会对 TypeScript 编译器无法自动推断出的类型进行假设。在这时,通过 asSomeOtherType 可以加快转换速度,而不必修改 tsconfig 中的设置。



    • 为什么不该这样做


    Type Guard 会确保所有检查都是明确的。

    5. 测试中的 as any



    • 这种习惯看起来是什么样的


    编写测试时创建不完整的用例。


    interface User {
    id: string
    firstName: string
    lastName: string
    email: string
    }

    test('createEmailText returns text that greats the user by first name', () => {
    const user: User = {
    firstName: 'John'
    } as any

    expect(createEmailText(user)).toContain(user.firstName)
    }


    • 应该怎样


    如果你需要模拟测试数据,请将模拟逻辑移到要模拟的对象旁边,并使其可重用。


    interface User {
    id: string
    firstName: string
    lastName: string
    email: string
    }

    class MockUser implements User {
    id = 'id'
    firstName = 'John'
    lastName = 'Doe'
    email = 'john@doe.com'
    }

    test('createEmailText returns text that greats the user by first name', () => {
    const user = new MockUser()

    expect(createEmailText(user)).toContain(user.firstName)
    }


    • 为什么会有这种坏习惯


    在给尚不具备广泛测试覆盖条件的代码编写测试时,通常会存在复杂的大数据结构,但要测试的特定功能仅需要其中的一部分。短期内不必关心其他属性。



    • 为什么不该这样做


    在某些情况下,被测代码依赖于我们之前认为不重要的属性,然后需要更新针对该功能的所有测试。


    6. 可选属性



    • 这种习惯看起来是什么样的


    将属性标记为可选属性,即便这些属性有时不存在。


    interface Product {
    id: string
    type: 'digital' | 'physical'
    weightInKg?: number
    sizeInMb?: number
    }


    • 应该怎样


    明确哪些组合存在,哪些不存在。


    interface Product {
    id: string
    type: 'digital' | 'physical'
    }

    interface DigitalProduct extends Product {
    type: 'digital'
    sizeInMb: number
    }

    interface PhysicalProduct extends Product {
    type: 'physical'
    weightInKg: number
    }


    • 为什么会有这种坏习惯


    将属性标记为可选而不是拆分类型更容易,并且产生的代码更少。它还需要对正在构建的产品有更深入的了解,并且如果对产品的设计有所修改,可能会限制代码的使用。



    • 为什么不该这样做


    类型系统的最大好处是可以用编译时检查代替运行时检查。通过更显式的类型,能够对可能不被注意的错误进行编译时检查,例如确保每个 DigitalProduct 都有一个 sizeInMb。


    7. 用一个字母通行天下



    • 这种习惯看起来是什么样的


    用一个字母命名泛型


    function head<T> (arr: T[]): T | undefined {
    return arr[0]
    }


    • 应该怎样


    提供完整的描述性类型名称。


    function head<Element> (arr: Element[]): Element | undefined {
    return arr[0]
    }


    • 为什么会有这种坏习惯


    这种写法最早来源于 C++的范型库,即使是 TS 的官方文档也在用一个字母的名称。它也可以更快地输入,只需要简单的敲下一个字母 T 就可以代替写全名。



    • 为什么不该这样做


    通用类型变量也是变量,就像其他变量一样。当 IDE 开始向我们展示变量的类型细节时,我们已经慢慢放弃了用它们的名称描述来变量类型的想法。例如我们现在写代码用 constname ='Daniel',而不是 conststrName ='Daniel'。同样,一个字母的变量名通常会令人费解,因为不看声明就很难理解它们的含义。


    8. 对非布尔类型的值进行布尔检查



    • 这种习惯看起来是什么样的


    通过直接将值传给 if 语句来检查是否定义了值。


    function createNewMessagesResponse (countOfNewMessages?: number) {
    if (countOfNewMessages) {
    return `You have ${countOfNewMessages} new messages`
    }
    return 'Error: Could not retrieve number of new messages'
    }


    • 应该怎样


    明确检查我们所关心的状况。


    function createNewMessagesResponse (countOfNewMessages?: number) {
    if (countOfNewMessages !== undefined) {
    return `You have ${countOfNewMessages} new messages`
    }
    return 'Error: Could not retrieve number of new messages'
    }


    • 为什么会有这种坏习惯


    编写简短的检测代码看起来更加简洁,使我们能够避免思考实际想要检测的内容。



    • 为什么不该这样做


    也许我们应该考虑一下实际要检查的内容。例如上面的例子以不同的方式处理 countOfNewMessages 为 0 的情况。


    9. ”棒棒“运算符



    • 这种习惯看起来是什么样的


    将非布尔值转换为布尔值。


    function createNewMessagesResponse (countOfNewMessages?: number) {
    if (!!countOfNewMessages) {
    return `You have ${countOfNewMessages} new messages`
    }
    return 'Error: Could not retrieve number of new messages'
    }



    • 应该怎样


    明确检查我们所关心的状况。


    function createNewMessagesResponse (countOfNewMessages?: number) {
    if (countOfNewMessages !== undefined) {
    return `You have ${countOfNewMessages} new messages`
    }
    return 'Error: Could not retrieve number of new messages'
    }


    • 为什么会有这种坏习惯


    对某些人而言,理解 !!就像是进入 JavaScript 世界的入门仪式。它看起来简短而简洁,如果你对它已经非常习惯了,就会知道它的含义。这是将任意值转换为布尔值的便捷方式。尤其是在如果虚值之间没有明确的语义界限时,例如 null、undefined 和 ''。



    • 为什么不该这样做


    与很多编码时的便捷方式一样,使用 !!实际上是混淆了代码的真实含义。这使得新开发人员很难理解代码,无论是对一般开发人员来说还是对 JavaScript 来说都是新手。也很容易引入细微的错误。在对“非布尔类型的值”进行布尔检查时 countOfNewMessages 为 0 的问题在使用 !!时仍然会存在。


    10. != null



    • 这种习惯看起来是什么样的


    棒棒运算符的小弟 ! = null 使我们能同时检查 null 和 undefined。


    function createNewMessagesResponse (countOfNewMessages?: number) {
    if (countOfNewMessages != null) {
    return `You have ${countOfNewMessages} new messages`
    }
    return 'Error: Could not retrieve number of new messages'
    }



    • 应该怎样


    明确检查我们所关心的状况。


    function createNewMessagesResponse (countOfNewMessages?: number) {
    if (countOfNewMessages !== undefined) {
    return `You have ${countOfNewMessages} new messages`
    }
    return 'Error: Could not retrieve number of new messages'
    }



    • 为什么会有这种坏习惯


    如果你的代码在 null 和 undefined 之间没有明显的区别,那么 != null 有助于简化对这两种可能性的检查。



    • 为什么不该这样做


    尽管 null 在 JavaScript 早期很麻烦,但 TypeScript 处于 strict 模式时,它却可以成为这种语言中宝贵的工具。一种常见模式是将 null 值定义为不存在的事物,将 undefined 定义为未知的事物,例如 user.firstName=== null 可能意味着用户实际上没有名字,而 user.firstName=== undefined 只是意味着我们尚未询问该用户(而 user.firstName===的意思是字面意思是 ''。


    链接:https://juejin.cn/post/6951934677682749477


    收起阅读 »

    TypeScript进阶, 如何避免 any

    为什么会出现 any不知道如何准确的定义出类型,TS 报错了,用 any 能解决,便用 any 了觉得定义类型浪费时间,项目经理催的紧,工期紧张,any 更方便频繁使用 any&n...
    继续阅读 »

    为什么会出现 any

    • 不知道如何准确的定义出类型,TS 报错了,用 any 能解决,便用 any 了
    • 觉得定义类型浪费时间,项目经理催的紧,工期紧张,any 更方便

    频繁使用 any 的弊端

    • 不利于良好的编码习惯
    • 不利于项目的后续维护
    • 会出现很多本可避免的 bug

    非必要不使用 any 的好处

    • 良好的代码提示
    • 强大的静态类型检查
    • 可读性和可维护性

    所以,我们要对 AnyScript 说不!

    TS 容易出现 any 的场景梳理

    给 window 全局对象增加属性

    常常能见到这样的写法

    ;(<any>window).obj = {}(
    // 或
    window as any
    ).obj = {}

    这样做,在使用时和赋值时都需要断言一次,非常麻烦,并且使用时也不能得到代码提示

    正确的做法应该是

    1. 在项目全局的 xxx.d.ts 文件中配置如下代码
    interface Window {
    obj: {}
    }
    1. 在需要给 window 赋值的文件目录下级新建一个 @types 文件夹,并在其中新建 index.d.ts 文件,添加如下代码
    interface Window {
    obj: {}
    }

    方法 2 也会在全局的 window 上增加 obj 这一声明,如果新增属性使用的跨度比较大,则推荐放在项目的 index.d.ts 中更利于维护,两种方式都在全局给 window 添加了属性,但方法 1 能一眼看出项目中 window 中添加了什么属性


    正确使用可选链、非空断言

    错误的理解 typescript 的可选参数,而使用断言导致隐患

    const a: {
    b: string
    c?: {
    d: string
    }
    } = {
    b: "123",
    }

    console.log((<any>a).c.d) // 错误,这样访问会报错,应使用可选链
    console.log(a.c!.d) // 错误,ts 不会将错误抛出,但实际访问也会报错

    ! 非空断言与 as 有相似之处,主要用于断言某个属性的值不为 null 和 undefined,它不会影响最终的代码产物,只会影响代码编译过程中的类型校验

    ?. 可选链操作符 会影响编译后的代码产物,如:

    这段 ts 代码

    const a = {
    c: undefined,
    }

    const b = a?.c?.d

    会被编译为如下 js 代码

    "use strict"

    var _a$c

    const a = {
    c: undefined,
    }
    const b =
    a === null || a === void 0
    ? void 0
    : (_a$c = a.c) === null || _a$c === void 0
    ? void 0
    : _a$c.d

    将对象属性类型关联起来

    对象中有多个属性是联合类型,其中 a 属性和 b 属性是有关联的,a 为 1 时,b 为 stringa 为 2 时,b 为 number 我们通常是这样定义的

    const obj: {
    a: 1 | 2
    b: string | number
    } = {
    a: 1,
    b: "1.2"
    }


    那么使用时,会造成需要用断言来再次限定 b 的范围的情况,如下代码段所示

    if (obj.a === 1) {
    const [left, right] = (obj.b as string).split(".")
    }
    // 如果你偷懒,那可能又变成了这样的情况
    if (obj.a === 1) {
    const [left, right] = (obj.b as any).split(".")
    }

    有没有什么办法能让我们不再 as 一次呢?有

    const obj: {
    a: 1
    b: string
    } | {
    a: 2
    b: number
    } = {
    a: 1,
    b: "1.2"
    }
    // 你会发现这样定义了以后,不需要再次进行断言限定 obj.b 的范围
    if (obj.a === 1) {
    const [left, right] = obj.b.split(".") // 校验通过
    }

    如果我们把这样的方法应用到函数(也可以用重载实现)传参或组件传参,有意思的是它还能限定传参的范围, 函数组件实现:


    错误的传参,a 与 b 的类型不匹配,校验不通过

    确的传参,校验能通过

    注意:你不能将 props 解构出来,会导致两者的关系丢失
    const { a, b } = props // 错误,a 和 b 的类型关系丢失

    是否使用联合类型需要辩证的看待,在任何时候都用上述方法定义可能会造成一些臃肿


    巧用类型保护避免断言


    typescript 中,常用的类型保护为 typeofinstanceof、和 in 关键字
    掌握上述关键字较为容易,可通过文档了解
    还有一个关键字 is (类型谓词)是 typescript 提供的,是另一种“类型保护”(这种说法助于理解)


    类型谓词能让我们通过函数的形式做出复杂的类型检验的逻辑,一个使用类型谓词的函数的声明往往是如下形式:

    type X = xxxx // 某种类型
    function check(params): params is X

    理解起来就是如果 check 函数返回了真值,则参数 paramsX 类型,否则不一定是 X 类型


    设想一下如下场景,某个项目,既可能运行在微信网页中,也可能运行在其他 webview


    在微信网页中,微信客户端向 window 对象中注入了各种 native 方法,使用它的方式就是 window.wx.xxxx()


    在其他 webview 中,我们假设也有这样的 native 方法,并且使用它的方式为 window.webviewnative.xxxx()


    在 typescript 项目中,window 对象上并不会默认存在 wxwebviewnative 两个属性,参考 给 window 全局对象增加属性,我们能显示地为 wxwebviewnative 两个属性定义类型:

    interface Window {
    wx?: {
    xxxx: Function
    }
    webviewnative?: {
    xxxx: Function
    }
    }

    如果你不会这样做,那可能又会写成断言为 any(window as any).wx.xxxx()


    可以看到在上面的代码段中两个属性都被我定义为了可选属性,目的是为了在后续维护(迭代)中,防止不做判断直接链式调用


    在微信环境中 window.wx 一定存在,但 webviewnative 一定不存在,反之在其他的 webview 中,(见前文假设)window.webviewnative 一定存在


    在接口 interface 中,我们并不能动态的知晓和定义到底哪个存在


    你可以这样写

    if (typeof window.wx !== 'undefined') {
    window.wx.xxxx()
    } else {
    // not in wx
    }

    但是直接在 if 中写这样的表达式太过局限,或者 有很多方式都能判断是在微信环境中,会导致项目中充斥着五花八门的判断,类型谓词的好处就出来了


    function checkIsWxNativeAPICanUse(win: Window): win is { wx: Exclude<Window['wx'], undefined> } & Window {
    return typeof window.wx !== 'undefined'
    }
    // 使用
    if (checkIsWxNativeAPICanUse(window)) {
    window.wx.xxxx()
    }

    总结

    非必要少使用 any 既是良好的 ts 代码习惯的养成,也是对自己代码质量的较真

    原文:https://juejin.cn/post/6961985123923263525



    收起阅读 »

    iOS-拍照后裁剪,不可拖动照片的问题

    问题在项目中,选择照片或拍照的功能很长见,由于我之前采用系统自带的UIimagePickViewController遇到一点问题:1、使用拍照功能,进行截取时(allowEditing = YES)时,拍照完成的图片无法拖动,没有办法进行选择性的截取图片2、如...
    继续阅读 »

    问题

    在项目中,选择照片或拍照的功能很长见,由于我之前采用系统自带的UIimagePickViewController遇到一点问题:

    1、使用拍照功能,进行截取时(allowEditing = YES)时,拍照完成的图片无法拖动,没有办法进行选择性的截取图片
    2、如果使用选择相册功能,进入裁剪界面,图片是可以拖动的,唯独拍照之后进入裁剪界面无法拖动
    3、微信头像更换拍照好像也无法拖动,初步推测可能使用的系统自带的裁剪界面
    所以想来仔细研究一下UIImagePickViewController的属性和使用方法

    UIImagePickViewController

    UIImagePickerController是iOS系统提供的和系统的相册和相机交互的一个类,可以用来获取相册的照片,也可以调用系统的相机拍摄照片或者视频。该类的继承结构是:

    UIImagePickerController-->UINavigationController-->UIViewController-->UIResponder-->NSObject

    官方文档中对于该类的说明是:

    该类只支持竖屏模式,为了保证该类被原样使用,它不支持子类,并且它的视图层次是私有的不能被修改,只支持自定义cameraOverlayView属性来展示更多信息以及和用户的交互。

    由于该类继承自UINavgationController,所以在使用过程中一般实现UIImagePickerControllerDelegate和UINavigationControllerDelegate这两个代理,可以利用navgation的push 和pop操作自定义界面实现更复杂的交互效果。

    下面具体分析该类的一些方法和属性.

    UIImagePickViewController之常用属性

    @property (nullable, nonatomic, weak) id <UINavigationControllerDelegate, UIImagePickerControllerDelegate> delegate;

    该对象的代理需要实现UINavigationControllerDelegate和UIImagePickerControllerDelegate协议,nullable是xcode6.3之后引入的nullability annotations特性,主要用于在OC和swift之间的转换。这一特性主要包含两个新的类型注释nullable和nonnull,用于表示对象是否可以是NULL或nil

    @property (nonatomic) UIImagePickerControllerSourceType sourceType; // default value is UIImagePickerControllerSourceTypePhotoLibrary.

    sourceType用于指定要访问的系统的媒体类型。UIImagePickerControllerSourceType支持以下3种枚举类型,默认值是图片库

    typedef NS_ENUM(NSInteger, UIImagePickerControllerSourceType) { UIImagePickerControllerSourceTypePhotoLibrary,//照片库模式。图像选取控制器以该模式显示时会浏览系统照片库的根目录。 UIImagePickerControllerSourceTypeCamera, //相机模式,图像选取控制器以该模式显示时可以进行拍照或摄像。 UIImagePickerControllerSourceTypeSavedPhotosAlbum //相机胶卷模式,图像选取控制器以该模式显示时会浏览相机胶卷目录。};

    1、PhotoLibrary代表系统照片应用对应的相薄,包含照片流和其它自定义的相册
    2、PhotosAlbum则对应系统照片应用的照片,包含用设备拍摄的所有照片流。
    3、Camera则代表相机的摄像头。

    @property (nonatomic, copy) NSArray<NSString *> *mediaTypes;

    mediaTypes用于设置相机支持的功能,拍照或者是视频,返回值类型可以是kUTTypeMovie视频和kUTTypeImage拍照

    1、kUTTypeMovie包含

    const CFStringRef kUTTypeAudiovisualContent ;抽象的声音视频
    const CFStringRef kUTTypeMovie ;抽象的媒体格式(声音和视频)
    const CFStringRef kUTTypeVideo ;只有视频没有声音
    const CFStringRef kUTTypeAudio ;只有声音没有视频
    const CFStringRef kUTTypeQuickTimeMovie ;
    const CFStringRef kUTTypeMPEG ;
    const CFStringRef kUTTypeMPEG4 ;
    const CFStringRef kUTTypeMP3 ;
    const CFStringRef kUTTypeMPEG4Audio ;
    const CFStringRef kUTTypeAppleProtectedMPEG4Audio;

    2、kUTTypeImage包含

    const CFStringRef kUTTypeImage ;抽象的图片类型
    const CFStringRef kUTTypeJPEG ;
    const CFStringRef kUTTypeJPEG2000 ;
    const CFStringRef kUTTypeTIFF ;
    const CFStringRef kUTTypePICT ;
    const CFStringRef kUTTypeGIF ;
    const CFStringRef kUTTypePNG ;
    const CFStringRef kUTTypeQuickTimeImage ;
    const CFStringRef kUTTypeAppleICNS const CFStringRef kUTTypeBMP;
    const CFStringRef kUTTypeICO;
    @property (nonatomic) BOOL showsCameraControls NS_AVAILABLE_IOS(3_1);
    @property (nonatomic) BOOL allowsEditing NS_AVAILABLE_IOS(3_1); // replacement for -allowsImageEditing; default value is NO.
    @property (nonatomic) BOOL allowsImageEditing NS_DEPRECATED_IOS(2_0, 3_1);

    1、showsCameraControls用于指定拍照时下方的工具栏是否显示
    2、allowImageEditing在iOS3.1就已废弃,取而代之的是allowEditing,
    3、allowEditing表示拍完照片或者从相册选完照片后,是否跳转到编辑模式对图片裁剪,只有在showsCameraControls为YES时才有效果。

    @property (nonatomic) UIImagePickerControllerCameraCaptureMode cameraCaptureMode NS_AVAILABLE_IOS(4_0); // default is UIImagePickerControllerCameraCaptureModePhoto
    @property (nonatomic) UIImagePickerControllerCameraDevice cameraDevice NS_AVAILABLE_IOS(4_0); // default is UIImagePickerControllerCameraDeviceRear
    @property (nonatomic) UIImagePickerControllerCameraFlashMode cameraFlashMode
    @property (nonatomic) CGAffineTransform cameraViewTransform NS_AVAILABLE_IOS(3_1); // set the transform of the preview view.
    @property (nullable, nonatomic,strong) __kindof UIView *cameraOverlayView NS_AVAILABLE_IOS(3_1); // set a view to overlay the preview view.

    当sourceType是camera的时候,这几个属性有效,否则抛出异常。

    · cameraCaptureMode捕捉模式指定的是相机是拍摄照片还是视频,它的枚举类型如下:

    NS_ENUM(NSInteger, UIImagePickerControllerCameraCaptureMode) { 
    UIImagePickerControllerCameraCaptureModePhoto,//photo
    UIImagePickerControllerCameraCaptureModeVideo//video
    };

    · cameraDevice指定拍摄的摄像头位置,是使用前置摄像头还是后置摄像头,它的枚举类型有:

    typedef NS_ENUM(NSInteger, UIImagePickerControllerCameraDevice) { 
    UIImagePickerControllerCameraDeviceRear,//后摄像头(默认)
    UIImagePickerControllerCameraDeviceFront//前摄像头
    };

    · cameraFlashMode用于指定闪光灯模式,它的枚举类型如下:

    typedef NS_ENUM(NSInteger, UIImagePickerControllerCameraFlashMode) { 
    UIImagePickerControllerCameraFlashModeOff = -1,//关闭闪关灯
    UIImagePickerControllerCameraFlashModeAuto = 0,//自动
    UIImagePickerControllerCameraFlashModeOn = 1//开启闪关灯
    };

    · cameraViewTransform该结构体可以用于指定拍摄时View的一些形变属性,如旋转缩放等。当showsCameraControls为NO,系统的工具栏隐藏时,我们可以自定义背景View赋值给cameraOverlayView添加到拍摄时的预览视图之上。

    @property(nonatomic) NSTimeInterval videoMaximumDuration NS_AVAILABLE_IOS(3_1); // default value is 10 minutes.
    @property(nonatomic) UIImagePickerControllerQualityType videoQuality NS_AVAILABLE_IOS(3_1);

    · videoMaximumDuration用于设置视频拍摄模式下最大拍摄时长,默认值是10分钟。
    · videoQuality表示拍摄的视频质量设置,默认是Medium即表示中等质量。 videoQuality支持的枚举类型如下:

    typedef NS_ENUM(NSInteger, UIImagePickerControllerQualityType) { 
    UIImagePickerControllerQualityTypeHigh = 0, // 高清模式
    UIImagePickerControllerQualityTypeMedium = 1, //中等质量,适于WIFI传播
    UIImagePickerControllerQualityTypeLow = 2, //低等质量,适于蜂窝网络传输
    UIImagePickerControllerQualityType640x480 NS_ENUM_AVAILABLE_IOS(4_0) = 3, // VGA 质量
    UIImagePickerControllerQualityTypeIFrame1280x720 NS_ENUM_AVAILABLE_IOS(5_0) = 4,//1280*720的分辨率
    UIImagePickerControllerQualityTypeIFrame960x540 NS_ENUM_AVAILABLE_IOS(5_0) = 5,//960*540分辨率
    };

    UIImagePickViewController之类方法

    @interface UIImagePickerController : UINavigationController <NSCoding>
    + (BOOL)isSourceTypeAvailable:(UIImagePickerControllerSourceType)sourceType; // returns YES if source is available (i.e. camera present)
    + (nullable NSArray<NSString *> *)availableMediaTypesForSourceType:(UIImagePickerControllerSourceType)sourceType; // returns array of available media types (i.e. kUTTypeImage)
    + (BOOL)isCameraDeviceAvailable:(UIImagePickerControllerCameraDevice)cameraDevice NS_AVAILABLE_IOS(4_0); // returns YES if camera device is available
    + (BOOL)isFlashAvailableForCameraDevice:(UIImagePickerControllerCameraDevice)cameraDevice NS_AVAILABLE_IOS(4_0); // returns YES if camera device supports flash and torch.
    + (nullable NSArray<NSNumber *> *)availableCaptureModesForCameraDevice:(UIImagePickerControllerCameraDevice)cameraDevice NS_AVAILABLE_IOS(4_0);

    isSourceTypeAvailable用于判断当前设备是否支持指定的sourceType,可以是照片库/相册/相机.
    isCameraDeviceAvailable判断当前设备是否支持前置摄像头或者后置摄像头
    isFlashAvailableForCameraDevice是否支持前置摄像头闪光灯或者后置摄像头闪光灯
    availableMediaTypesForSourceType方法返回所特定的媒体如相册/图片库/相机所支持的媒体类型数组,元素值可以是kUTTypeImage类型或者kUTTypeMovie类型的静态字符串,所以是NSString类型的数组
    availableCaptureModesForCameraDevice返回特定的摄像头(前置摄像头/后置摄像头)所支持的拍摄模式数值数组,元素值可以是UIImagePickerControllerCameraCaptureMode枚举里面的video或者photo,所以是NSNumber类型的数组

    UIImagePickViewController之对象方法

    - (void)takePicture NS_AVAILABLE_IOS(3_1);
    - (BOOL)startVideoCapture NS_AVAILABLE_IOS(4_0);
    - (void)stopVideoCapture NS_AVAILABLE_IOS(4_0);

    1、takePicture可以用来实现照片的连续拍摄,需要自己自定义拍摄的背景视图来赋值给cameraOverlayView,结合自定义overlayView实现多张照片的采集,在收到代理的didFinishPickingMediaWithInfo方法之后可以启动额外的捕捉。
    2、startVideoCapture用来判断当前是否可以开始录制视频,当视频正在拍摄中,设备不支持视频拍摄,磁盘空间不足等情况,该方法会返回NO.该方法结合自定义overlayView可以拍摄多部视频
    3、stopVideoCapture当你调用此方法停止视频拍摄时,它会调用代理的imagePickerController:didFinishPickingMediaWithInfo:方法

    UIImagePickViewController之代理方法

    @protocol UIImagePickerControllerDelegate<NSObject>
    @optional
    - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingImage:(UIImage *)image editingInfo:(nullable NSDictionary<NSString *,id> *)editingInfo NS_DEPRECATED_IOS(2_0, 3_0);
    - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info;
    - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker;
    @end

    1、imagePickerController:didFinishPickingImage:editingInfo:在iOS3.0中已废弃,不再使用。
    2、当用户取消选取的内容时会调用DidCancel方法,默认实现销毁弹出的视图。
    3、当完成内容的选取时会调用didFinishPickingMediaWithInfo方法,默认info字典的key值可以是以下类型:

    UIKIT_EXTERN NSString *const UIImagePickerControllerMediaType; //指定用户选择的媒体类型
    UIKIT_EXTERN NSString *const UIImagePickerControllerOriginalImage; // 原始图片
    UIKIT_EXTERN NSString *const UIImagePickerControllerEditedImage; // 修改后的图片
    UIKIT_EXTERN NSString *const UIImagePickerControllerCropRect; // 裁剪尺寸
    UIKIT_EXTERN NSString *const UIImagePickerControllerMediaURL; // 媒体的URL
    UIKIT_EXTERN NSString *const UIImagePickerControllerReferenceURL NS_AVAILABLE_IOS(4_1); // 原件的URL
    UIKIT_EXTERN NSString *const UIImagePickerControllerMediaMetadata //当数据来源是相机的时候获取到的静态图像元数据,可以使用phtoho框架进行处理

    UIImagePickViewController之C语言函数(保存图片或视频)

    UIKIT_EXTERN void UIImageWriteToSavedPhotosAlbum(UIImage *image, __nullable id completionTarget, __nullable SEL completionSelector, void * __nullable contextInfo);
    UIKIT_EXTERN BOOL UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(NSString *videoPath) NS_AVAILABLE_IOS(3_1);
    UIKIT_EXTERN void UISaveVideoAtPathToSavedPhotosAlbum(NSString *videoPath, __nullable id completionTarget, __nullable SEL completionSelector, void * __nullable contextInfo) NS_AVAILABLE_IOS(3_1);

    1、UIImageWriteToSavedPhotosAlbum用来保存照片到相册,seletor应该设置为- (void)image:(UIImage )image didFinishSavingWithError:(NSError )error contextInfo:(void )contextInfo;当照片保存到相册完成时,会调用该方法通知你。
    2、UIVideoAtPathIsCompatibleWithSavedPhotosAlbum会返回布尔类型的值判断该路径下的视频能否保存到相册,视频需要先存储到沙盒文件再保存到相册,而照片是可以直接从代理完成的回调info字典里面获取到。
    3、UISaveVideoAtPathToSavedPhotosAlbum用来保存视频到相册,seletor应该设置为- (void)video:(NSString )videoPath didFinishSavingWithError:(NSError )error contextInfo:(void )contextInfo;当视频保存到相册或出错时会调用该方法通知你。
    这三个方法一般是在代理的完成方法didFinishPickingMediaWithInfo里面配合使用。
    以上便是系统UIImagePickViewController的所有属性和方法的简单介绍

    问题解决方案

    由于使用设置allowEditing = YES属性后,开启摄像头拍照后直接进入系统自定义的裁剪界面,但是在这个界面,从摄像头拍摄过来的照片无法拖动裁剪,仅仅从相册选择过来的照片是可以拖动进行裁剪的,那么我们就决定裁剪界面不使用系统自带的,而自定义一个裁剪界面.

    在UIImagePickViewController的代理方法里

    #pragma UIImagePickerController Delegate
    - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
    {
    UIImage *portraitImg = [info objectForKey:@"UIImagePickerControllerOriginalImage"];
    YYImageClipViewController *imgClipVC = [[YYImageClipViewController alloc] initWithImage:portraitImg cropFrame:CGRectMake(0, 100.0f, self.view.frame.size.width, self.view.frame.size.width) limitScaleRatio:3.0];
    imgClipVC.delegate = self;
    [picker pushViewController:imgClipVC animated:NO];
    }

    - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
    {
    [picker dismissViewControllerAnimated:YES completion:nil];
    }

    HZImageCropperViewController是我自定义的一个裁剪界面,通过这个控制器的两个代理方法,得到裁剪后的照片

    - (void)imageClip:(YYImageClipViewController *)clipViewController didFinished:(UIImage *)editedImage 
    {
    //保存图片
    NSString *imageFilePath = [UIImage saveImage:editedImage];
    //上传七牛服务器
    [self upImageFilePath:imageFilePath];
    //隐藏裁剪界面
    [clipViewController dismissViewControllerAnimated:YES completion:nil];
    }

    - (void)imageClipDidCancel:(YYImageClipViewController *)clipViewController
    {
    [clipViewController dismissViewControllerAnimated:YES completion:nil];
    }

    下面附上一个demo,里面包含裁剪界面的源代码,可供大家参考
    自定义裁剪界面demo

    转自:https://www.jianshu.com/p/9474ca73e269

    收起阅读 »

    【环信大学】深入浅出Runtime(2)

    逻辑图终端代码及源码clang 命令行及源码.zip


    逻辑图


    终端代码及源码

    clang 命令行及源码.zip




    用ts类型系统实现斐波那契数列

    一、我们要做什么我们的目的是想要通过TypeScript的类型声明式语法,编程实现一个斐波那契数列算法。换句话说,类似于用现有的机器码到指令集、二进制到十进制、汇编语言到高级编程语言的过程,让类型定义语法也可以实现编程。最终我们要实现的斐波那契数列代码是这样的...
    继续阅读 »

    一、我们要做什么

    我们的目的是想要通过TypeScript的类型声明式语法,编程实现一个斐波那契数列算法。换句话说,类似于用现有的机器码到指令集、二进制到十进制、汇编语言到高级编程语言的过程,让类型定义语法也可以实现编程。

    最终我们要实现的斐波那契数列代码是这样的?

    const fib = (n: number): number => n <= 1 ? n : fib(n - 1) + fib(n - 2);

    for (let i = 0; i < 10; i++) {
    console.log(i, fib(i));
    }

    运行结果如下:


    程序完全没问题,完结撒花!

    开玩笑的,上面是只一个用了TypeScript类型定义的JavaScript写法,我们其实真正想这样做↓↓↓, 也就是使用TS Type解决FIbonacci

    import { Fib, Add } from './fib-type';

    type one = Fib<1>;
    type zero = Fib<0>;
    type Two = Add<one, one>;
    type Five = Add<Two, Add<Two, one>>;
    type Fib5 = Fib<Five>;
    type Fib9 = Fib<9>;
    type r0 = Fib<Zero>; // type r0= 0
    type r1 = Fib<One>; // type r1 = 1
    type r2 = Fib<Two>; // type r2 = 1
    type r3 = Fib<3>; // type r3 = 2
    type r4 = Fib<4>; // type r4 = 3
    type r5 = Fib<5>; // type r5 = 5
    type r6 = Fib<6>; // type r6 = 8
    type r9 = Fib<9>; // type r9 = 34
    type sum = Add<r9, r6>; // type sum = 42

    二、我们该怎么做

    要想实现斐波那契数列,参考一开始的代码,有基本的比较, 加法, 循环语法, 所以我们也需要使用类型系统依次实现这三种功能

    2.1 加法的实现

    为了实现加法, 需要先实现一些工具类型

    // 元组长度
    type Length<T extends any[]> = T['length'];
    type one = 1

    // 使用extends实现数字相等的比较
    type a111 = 0 extends one ? true : false // type a111 = false
    type a112 = 1 extends one ? true : false // type a112 = true

    range的实现是递归实现的

    // 伪代码
    function range(n, list=[]){
    if(n<=0) return list.length
    return range(n-1, [1, ...list])
    }

    TypeScript的限制, 没有循环, 只能用递归代替循环, 后面会有几个类似的写法, 记住一点:递归有几个出口, 对象就有几个 key, 每个 key 就是一个条件

    // 创建指定长度的元组, 用第二个参数携带返回值
    type Range<T extends Number = 0, P extends any[] = []> = {
    0: Range<T, [any, ...P]>;
    1: P;
    }[Length<P> extends T ? 1 : 0];

    // 拼接两个元组
    type Concat<T extends any[], P extends any[]> = [...T, ...P];

    type t1 = Range<3>;
    // type t1 = [any, any, any]

    type Zero = Length<Range<0>>;
    // type Zero = 0
    type Ten = Length<Range<10>>;
    // type Ten = 10

    type Five = Length<Range<5>>;
    // type Five = 5

    type One = Length<Range<1>>;

    有了上面的工具语法,那么实现加法就比较容易了, 只需要求两个元组合并后的长度

    type Add<T extends number, P extends number> = Length<
    Concat<Range<T>, Range<P>>
    >;
    type Two = Add<One, One>;
    // type Two = 2
    type Three = Add<One, Two>;
    // type Three = 3

    有了加法,该如何实现减法呢?一般减法和除法都比加法难, 所以我们需要更多的工具类型函数

    2.2 工具函数

    2.2.1 实现一些基本工具类型

    • Shift:删除第一个元素
    • Append:在元组末尾插入元素
    • IsEmpty / NotEmpty:判断列表为空
    // 去除元组第一个元素 [1,2,3] -> [2,3]
    type Shift<T extends any[]> = ((...t: T) => any) extends (
    _: any,
    ...Shift: infer P
    ) => any
    ? P
    : [];

    type pp = Shift<[number, boolean,string, Object]>
    // type pp = [boolean, string, Object]

    // 向元组中追加
    type Append<T extends any[], E = any> = [...T, E];
    type IsEmpty<T extends any[]> = Length<T> extends 0 ? true : false;
    type NotEmpty<T extends any[]> = IsEmpty<T> extends true ? false : true;
    type t4 = IsEmpty<Range<0>>;
    // type t4 = true

    type t5 = IsEmpty<Range<1>>;
    // type t5 = false

    2.2.2 逻辑类型

    • Anda && b
    // 逻辑操作
    type And<T extends boolean, P extends boolean> = T extends false
    ? false
    : P extends false
    ? false
    : true;
    type t6 = And<true, true>;
    // type t6 = true

    type t7 = And<true, false>;
    // type t7 = false

    type t8 = And<false, false>;
    // type t8 = false

    type t9 = And<false, true>;
    // type t9 = false

    2.2.3 小于等于

    伪代码: 主要思想是同时从列表中取出一个元素, 长度先到0的列表比较短

    function dfs (a, b){
    if(a.length && b.length){
    a.pop()
    b.pop()
    return dfs(a,b)
    }else if(a.length){
    a >= b
    }else if (b.length){
    b > a
    }
    }

    思想:将数字的比较转换为列表长度的比较

    // 元组的小于等于   T <= P, 同时去除一个元素, 长度先到0的比较小
    type LessEqList<T extends any[], P extends any[]> = {
    0: LessEqList<Shift<T>, Shift<P>>;
    1: true;
    2: false;
    }[And<NotEmpty<T>, NotEmpty<P>> extends true
    ? 0
    : IsEmpty<T> extends true
    ? 1
    : 2];


    // 数字的小于等于
    type LessEq<T extends number, P extends number> = LessEqList<Range<T>, Range<P>>;

    type t10 = LessEq<Zero, One>;
    // type t10 = true
    type t11 = LessEq<One, Zero>;
    // type t11 = false

    type t12 = LessEq<One, One>;
    // type t12 = true

    2.3 减法的实现

    减法有两个思路,列表长度相减求值和数字相减求值

    2.3.1 列表减法

    默认大减小, 小减大只需要判断下反着来, 然后加个符号就行了, 这里为了简单没有实现,可参考伪代码如下:

    // 伪代码
    const a = [1, 2, 3];
    const b = [4, 5];
    const c = [];
    while (b.length !== a.length) {
    a.pop();
    c.push(1);
    }// c.length === a.length - b.lengthconsole.log(c.length);

    // 元组的减法 T - P, 同时去除一个元素, 长度到0时, 剩下的就是结果, 这里使用第三个参数来携带结果, 每次做一次减法, 向第三个列表里面追加
    type SubList<T extends any[], P extends any[], R extends any[] = []> = {
    0: Length<R>;
    1: SubList<Shift<T>, P, Apped<R>>;
    }[Length<T> extends Length<P> ? 0 : 1];
    type t13 = SubList<Range<10>, Range<5>>;
    // type t13 = 5

    2.3.2 数字减法

    思想:将数字转成元组后再比较

    // 集合大小不能为负数, 默认大减小
    // 数字的减法
    type Sub<T extends number, P extends number> = {
    0: Sub<P, T>;
    1: SubList<Range<T>, Range<P>>;
    }[LessEq<T, P> extends true ? 0 : 1];

    type t14 = Sub<One, Zero>;
    // type t14 = 1
    type t15 = Sub<Ten, Five>;
    // type t15 = 5

    我们有了这些工具后, 就可以将一开始用JavaScript实现的斐波那契数列的实现代码,翻译为TypeScript类型编码

    三、Fib: JS函数 --> TS类型

    在JavaScript中,我们使用函数

    const fib = (n: number): number => n <= 1 ? n : fib(n - 1) + fib(n - 2);

    在TypeScript中,我们使用类型, 其实只是换了一种写法, 用类型函数描述运算, 万变不离其宗~

    由于TypeScript递归限制, 并不能求解非常大的项, 不过好玩就完事了~

    export type Fib<T extends number> = {
    0: T;
    1: Add<Fib<Sub<T, One>>, Fib<Sub<T, Two>>>;
    }[LessEq<T, One> extends true ? 0 : 1];

    type r0 = Fib<Zero>;
    // type r10= 0
    type r1 = Fib<One>;
    // type r1 = 1

    type r2 = Fib<Two>;
    // type r2 = 1

    type r3 = Fib<3>;
    // type r3 = 2

    type r4 = Fib<4>;
    // type r4 = 3

    type r5 = Fib<5>;
    //type r5 = 5

    type r6 = Fib<6>;
    // type r6 = 8

    四、总结


    看了TypeScript实现斐波纳切数列这一套操作有没有让你有体验到重回“实现流水线CPU”的实验室时光?


    IT在最近几十年的发展突飞猛进,越来越多的“程序员”加入到了互联网行业,在一些高级语言以及开发框架下,“程序员”的编码也只需要关注业务逻辑实现,很少有人会再去关注计算机底层是怎么实现加减乘除的,当然社会在进步,技术也在日新月异地迭代,偶尔驻足,回忆刚接触计算机编程,在命令行输出第一行“Hello World!”代码那时欣喜的自己,也许那是我们都回不去的青春...



    链接:https://juejin.cn/post/6965320374451961886
    收起阅读 »

    2021不得不学的Typescript

    ts作为一门新技术,这两年是越来越火,经过了一段时间的学习和理解之后,写了这篇文章,通过记录ts的核心知识点来带大家轻松掌握typescript,希望能够打动屏幕面前的你。Typescript基础语法TypeScript支持与JavaScript几乎相同的数据...
    继续阅读 »

    ts作为一门新技术,这两年是越来越火,经过了一段时间的学习和理解之后,写了这篇文章,通过记录ts的核心知识点来带大家轻松掌握typescript,希望能够打动屏幕面前的你。

    Typescript基础语法

    TypeScript支持与JavaScript几乎相同的数据类型,此外还提供了实用的枚举类型方便我们使用。接下来我们简单介绍一下这几种类型的用法.

    基础类型

    // 布尔类型
    let isFlag:boolean = true

    // 数值类型
    let myNumber:number = 24

    // 字符串类型
    let str:string = 'ykk'

    // 数组类型, 有两种表示方式,第一种可以在元素类型后面接上[],表示由此类型元素组成的一个数组
    let arr:number[] = [1,2,3]
    //当数组存在不同类型时
    let arr1: (number | string)[] = [1, '2', 3]

    // 数组类型, 使用数组泛型(有关泛型后面会详细分析)
    let arr:Array<number> = [4,5,6]

    // 元组类型, 允许表示一个已知元素数量和类型的数组,各元素的类型不必相同
    let yuan: [string, number];
    // 初始化yuan
    yuan = ['yuan', 12]; // 正确
    yuan = [12, 'yuan']; // 错误

    // 枚举类型, 可以为一组数值赋予友好的名字
    enum ActionType { online, offline, deleted }
    let action:ActionType = ActionType.offline // 1

    // any, 表示任意类型, 可以绕过类型检查器对这些值进行检查
    let color:any = 1
    color = 'red'

    // void类型, 当一个函数没有返回值时,通常会设置其返回值类型是 void
    function getName(): void {
    console.log("This is my name");
    }

    // object类型, 表示非原始类型,也就是除number,string,boolean,symbol,null或undefined之外的类型
    let obj:object;
    obj = {num: 1}

    // 对象类型也可以写成
    const t = {
    name:string,
    age:number
    } = {
    name:'ykk',
    age:18
    }

    // 函数类型也属于一种对象类型(该实例返回值必须是number类型)
    const getTt:() => number = () => {
    return 123;
    }

    //never类型(永远不会执行never之后的逻辑)
    function errorEmitter(): never {
    throw new Error();
    console.log(123)
    }

    接口(Interface)

    interface Person {
    name: string;
    age: number;
    phone: number;
    }

    let man:Person = {
    name: 'ykk',
    age: 18,
    phone: 13711111111
    }

    类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以。其次我们还可以定义可选属性只读属性。 可选属性表示了接口里的某些属性不是必需的,所以可以定义也可以不定义。·可读属性·使得接口中的某些属性只能读取而不能赋值,如下:

    interface Person {
    name: string;
    age?: number;
    readonly phone: number;
    }

    在实际场景中, 我们往往还会遇到不确定属性名和属性值类型的情况, 这个时候我们可以利用索引签名来设置额外的属性和类型, 如下:

    interface Person {
    name: string;
    [propName: string]: any; //这里表示除了有name之外还可以有其他的任何属性,但是属性名必须是string类型,属性值任意类型都可以
    }

    当然接口也可以进行继承

    //此时human这个interface就拥有Person的所有属性,并且在调用的时候也必须满足Proson的要求,否则就会报错
    interface human extends Person {
    weight:55
    }

    和js的class一致, typescript的类有公共(public)私有(private)受保护(protected)的访问类型。

    • public 在TypeScript里,成员都默认为 public,表示允许它在定义类的外部被调用

    • private 表示它能在定义的类内使用,不能在该类的外部使用

    • protected 和private类似, 但是protected允许在类内及继承的子类中使用

    class Person {
    public name:string = 'ykk';
    private age:number = 18;
    protected weight:number = 55;
    constructor(){

    }
    }

    class Man extends Person {
    public say(){
    return this.weight
    }
    }

    let p = new Person()
    let m = new Man()
    console.log(p.name) //ykk
    console.log(p.age) //报错age是private属性
    console.log(m.say()) //55

    由于在js中,getter 和 setter不能直接使用,我们需要通过一个Object.defineProperty来定义触发,那么在ts中就简单多了在类中直接能声明:

    class Person {
    private _food: string = 'apple'
    get food() {
    return this._food
    }
    set food(name: string) {
    this._food = name
    }
    }
    let p = new Person()
    console.log(p.food) //apple
    p.food="cookie" //这里重新设置food

    typescript中static这个关键字是把这个方法或者属性直接挂在类上,而不是挂在new出来的实例。设计模式中经典的单例模式,用它最合适不过了!

    class Demo {
    private static instance: Demo;
    peivate contructor(public name:string){}

    static getInstance(){
    if(!this.instance){
    this.instance = new Demo('ykk')
    }
    return this.instance;
    }
    }

    const demo1 = Demo.getInstance();
    const demo2 = Demo.getInstance();
    console.log(demo1.name)
    console.log(demo2.name)

    抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。 不同于接口,抽象类可以包含成员的实现细节。 abstract关键字是用于定义抽象类和在抽象类内部定义抽象方法, 这里需要注意的是不能创建一个抽象类的实例

    abstract class MyAbstract {
    constructor(public name: string) {}
    say(): void {
    console.log('say name: ' + this.name);
    }
    abstract sayBye(): void; // 必须在派生类中实现
    }

    class SubMyAbstract extends MyAbstract {
    constructor() {
    super('ykk'); // 在派生类的构造函数中必须调用 super()
    }
    sayBye(): void {
    console.log('bye');
    }
    getOther(): void {
    console.log('loading...');
    }
    }

    let department: MyAbstract; // 允许创建一个对抽象类型的引用
    department = new SubMyAbstract(); // 允许对一个抽象子类进行实例化和赋值
    department.say();
    department.sayBye();

    department = new MyAbstract(); // 错误: 不能创建一个抽象类的实例
    department.getOther(); // 错误: 方法在声明的抽象类中不存在

    Typescript进阶语法

    联合类型和类型保护

    所谓联合类型是用于限制传入的值的类型只能是 | 分隔的每个类型,所以 number | string | boolean表示一个值可以是 number, string,或 boolean。例如:

    interface person1 {
    name: string;
    age: number;
    }

    interface person2 {
    hby: string;
    age: number;
    }
    let man:person1 | person2

    如果一个值是联合类型,那么我们只能访问它们中共有的部分(共有的属性与方法),由于只能访问共有,导致我们在想要访问某一个的时候ts会提示报错,这时我们就需要类型保护

    let man:person1 | person2;
    man = {
    name: 'ykk',
    age: 18,
    hby: 'basketball'
    }
    //使用as直接断言,告诉ts在哪里去找
    if((me as person1).name) {
    console.log((me as person1).name);
    }

    if((me as person1).name) {
    console.log((me as person2).hby);
    }
    //使用in
    if(('name' in me)) {
    console.log(me.name);
    }

    if('hby' in me) {
    console.log(me.hby);
    }
    //使用typeof
    function add(one:number|string,two:number|string){
    if(typeof one=="string"||typeof two=="string"){
    retrun `${one}${two}`
    }
    retrun one+two
    }
    //使用instanceof
    class a{
    num:1
    }
    function b(obj:object|a){
    if(obj instanceof a){
    retrun obj.num
    }
    }

    泛型

    什么是泛型呢,我的理解就是泛指的类型,那他在ts中应该怎么写呢?

    //定义是用尖括号表示一个变量
    function iSay<T>(arg: T): T {
    return arg;
    }
    // 调用的时候去声明类型
    let come = iSay<number>(123);

    当然了泛型有多种使用方式,接下来咱们一一探索。

    函数泛型
    //传入一个数组
    function say<T>(arr:T[]){
    ...
    };
    say<number>([11,22,33]);

    //传入多个泛型
    function say<T, F>(name:T, age:F){
    ...
    };
    say<string, number>('ykk', 18);
    类中的泛型
    class say<T>{
    constructor(name:T){
    ...
    }
    }
    var t = new say<string>("1")
    泛型的继承
    class say<T extends number|string>{
    constructor(one:T){
    ...
    }
    }
    var t = new say<string>("1") //这时候表示泛型只能是number类型或者string类型其中的一种,否则会报错
    泛型中使用keyof

    泛型中使用keyof顾名思义就是遍历一个interface,并且每次的泛型类型就是当前interface的key.interface persons {

        name:string,
    age:number,
    get:boolean
    }
    var p = {
    name:'ykk',
    age:18,
    get:false
    }
    function add<T extends keyof persons>(key:T):persons[T]{
    return p[key]
    }
    //如此一来我们便能知道返回值的准确类型了
    var p1 = add('name')
    console.log(p1) //ykk

    命名空间

    ts在我们使用的时候如果用面向对象的方式声明多个类生成实例的时候,你会发现在全局就会多出几个实例,这样就会导致全局污染,如此一来,我们便需要namespace这个关键字,来防止全局污染。

    namespace Main{
    class circle {
    ...
    }

    class rect {
    ...
    }

    //如果想要导出给外部使用,需要导出
    export class san{
    ...
    }
    }
    //这样在全局只会有一个Main供我们使用了

    声明全局变量

    对于使用未经声明的全局函数或者全局变量, typescript往往会报错。最常见的例子就是我们在引入js文件的时候,往往会报下面的错误:


    这个时候有两种解决方式:

    • 按照提示执行相对于的npm install @types/xxx的命令。
    • 可以在对应位置添加xxx.d.ts文件, 并在里面声明我们所需要的变量, ts会自动检索到该文件并进行解析,如下:
    //定义一个全局类型, 并编写相应的逻辑让ts识别相应的js语法
    declare var superagent;
    ...



    原文:https://juejin.cn/post/6966151454914510878

    收起阅读 »

    iOS 详解socket编程[oc]粘包、半包处理

    在做socket编程时,如果是做tcp连接,那就不可避免的会遇到粘包与半包的问题。粘包 就是多组数据被一并接收了,粘在了一起,无法做划分;半包 就是有数据接收不完整,无法处理。要解决粘包、半包的问题,一般在设计数据(消息)格式时会约定好一个字段专门用于描述数据...
    继续阅读 »

    在做socket编程时,如果是做tcp连接,那就不可避免的会遇到粘包与半包的问题。

    粘包 就是多组数据被一并接收了,粘在了一起,无法做划分;

    半包 就是有数据接收不完整,无法处理。

    要解决粘包、半包的问题,一般在设计数据(消息)格式时会约定好一个字段专门用于描述数据包的长度,这样就使数据有了边界,依靠这个边界,就能把每组数据划分出来,数据不完整时也能获知数据的缺失。

    (当然也可以把数据设计成定长数据,但这样不够灵活;或者用\n,\r这类字符作为数据划分依据,但不直观、不明确,同时也不灵活)

    举个栗子:

    消息=消息头+消息体。消息头用于描述消息本身的基本信息,消息体则为消息的具体内容


    如上图所示,假如我们的一个消息是这么定义的

    消息头 = msgId(4B)+version(2B)+len(4B),共占用10字节

    消息体 =  len中描述的16字节长

    所以这条消息的长度就是 26字节

    可以看到,要想知道一条完整数据的边界,关键就是消息头中的len字段

    假如我们现在接收到的数据是这样的:


    这个情况下即包含了粘包,也出现了半包的情况,三个数据包粘在了一起,最后一个数据包没有接收完全,出现了半包的情况,看看代码如何处理

    - (void)onSocket:(AsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag 
    {
    while (_readBuf.length >= 10)//因为头部固定10个字节,数据长度至少要大于10个字节,我们才能得到完整的消息描述信息
    {
    NSData *head = [_readBuf subdataWithRange:NSMakeRange(0, 10)];//取得头部数据
    NSData *lengthData = [head subdataWithRange:NSMakeRange(6, 4)];//取得长度数据
    NSInteger length = [[[NSString alloc] initWithData:lengthData encoding:NSUTF8StringEncoding] integerValue];//得出内容长度
    NSInteger complateDataLength = length + 10;//算出一个包完整的长度(内容长度+头长度)
    if (_readBuf.length >= complateDataLength)//如果缓存中数据够一个整包的长度
    {
    NSData *data = [_readBuf subdataWithRange:NSMakeRange(0, complateDataLength)];//截取一个包的长度(处理粘包)
    [self handleTcpResponseData:data];//处理包数据
    //从缓存中截掉处理完的数据,继续循环
    _readBuf = [NSMutableData dataWithData:[_readBuf subdataWithRange:NSMakeRange(complateDataLength, _readBuf.length - complateDataLength)]];
    }
    else//如果缓存中的数据长度不够一个包的长度,则包不完整(处理半包,继续读取)
    {
    [_socket readDataWithTimeout:-1 buffer:_readBuf bufferOffset:_readBuf.length tag:0];//继续读取数据
    return;
    }
    }
    //缓存中数据都处理完了,继续读取新数据
    [_socket readDataWithTimeout:-1 buffer:_readBuf bufferOffset:_readBuf.length tag:0];//继续读取数据
    }


    摘自:https://www.jb51.net/article/105278.htm


    收起阅读 »

    Socket简析与iOS实现

    Socket的基本概念1.定义网络上两个程序通过一个双向通信连接实现数据交互,这种双向通信的连接叫做Socket(套接字)。2.本质网络模型中应用层与TCP/IP协议族通信的中间软件抽象层,是它的一组编程接口(API),也即对TCP/IP的封装。TCP/IP也...
    继续阅读 »

    Socket的基本概念

    1.定义

    网络上两个程序通过一个双向通信连接实现数据交互,这种双向通信的连接叫做Socket(套接字)。

    2.本质

    网络模型中应用层与TCP/IP协议族通信的中间软件抽象层,是它的一组编程接口(API),也即对TCP/IP的封装。TCP/IP也要提供可供程序员做网络开发所用的接口,即Socket编程接口。


    3.要素

    Socket是网络通信的基石,是支持TCP/IP协议的网络通信的基本操作单元,包含进行网络通信的必须的五种信息:

    • 连接使用的协议
    • 本地主机的IP地址
    • 本地进程的协议端口
    • 远程主机的IP地址
    • 远程进程的协议端口

    4.特性

    Socket可以支持不同的传输协议(TCP或UDP),当使用TCP协议进行连接时,该Socket连接就是一个TCP连接;同理,当使用UDP协议进行连接时,该Socket连接就是一个UDP连接。

    多个TCP连接或多个应用程序进程可能需要通过同一个TCP协议端口传输数据。为了区别不同的应用程序进程和连接,计算机操作系统为应用程序与TCP/IP协议交互提供了套接字(Socket)接口。应用层可以和传输层通过Socket接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。

    5.连接

    建立Socket连接至少需要一对套接字,分别运行于服务端(ServerSocket)和客户端(ClientSocket)。套接字直接的连接过程氛围三个步骤:

    Step 1 服务器监听

    服务端Socket始终处于等待连接状态,实时监听是否有客户端请求连接。

    Step 2 客户端请求

    客户端Socket提出连接请求,指定服务端Socket的地址和端口号,这时就可以向对应的服务端提出Socket连接请求。

    Step 3 连接确认

    当服务端Socket监听到客户端Socket提出的连接请求时作出响应,建立一个新的进程,把服务端Socket的描述发送给客户端,该描述得到客户端确认后就可建立起Socket连接。而服务端Socket则继续处于监听状态,继续接收其他客户端Socket的请求。

    iOS客户端Socket的实现

    1. 数据流方式

    - (IBAction)connectToServer:(id)sender {
    // 1.与服务器通过三次握手建立连接
    NSString *host = @"192.168.1.58";
    int port = 1212;

    //创建一个socket对象
    _socket = [[GCDAsyncSocket alloc] initWithDelegate:self
    delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];

    NSError *error = nil;

    // 开始连接
    [_socket connectToHost:host
    onPort:port
    error:&error];

    if (error) {
    NSLog(@"%@",error);
    }
    }


    #pragma mark - Socket代理方法
    // 连接成功
    - (void)socket:(GCDAsyncSocket *)sock
    didConnectToHost:(NSString *)host
    port:(uint16_t)port {
    NSLog(@"%s",__func__);
    }


    // 断开连接
    - (void)socketDidDisconnect:(GCDAsyncSocket *)sock
    withError:(NSError *)err {
    if (err) {
    NSLog(@"连接失败");
    } else {
    NSLog(@"正常断开");
    }
    }


    // 发送数据
    - (void)socket:(GCDAsyncSocket *)sock
    didWriteDataWithTag:(long)tag {

    NSLog(@"%s",__func__);

    //发送完数据手动读取,-1不设置超时
    [sock readDataWithTimeout:-1
    tag:tag];
    }

    // 读取数据
    -(void)socket:(GCDAsyncSocket *)sock
    didReadData:(NSData *)data
    withTag:(long)tag {

    NSString *receiverStr = [[NSString alloc] initWithData:data
    encoding:NSUTF8StringEncoding];

    NSLog(@"%s %@",__func__,receiverStr);
    }

    2.基于第三方开源库CocoaAsyncSocket

    2.1客户端通过地址和端口号与服务端建立Socket连接,并写入相关数据。

    - (void)connectToServerWithCommand:(NSString *)command
    {
    _socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
    [_socket setUserData:command];

    NSError *error = nil;
    [_socket connectToHost:WIFI_DIRECT_HOST onPort:WIFI_DIRECT_PORT error:&error];
    if (error) {
    NSLog(@"__connect error:%@",error.userInfo);
    }

    [_socket writeData:[command dataUsingEncoding:NSUTF8StringEncoding] withTimeout:10.0f tag:6];
    }

    2.2 实现CocoaAsyncSocket的代理方法

    #pragma mark - Socket Delegate

    - (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
    {
    NSLog(@"Socket连接成功:%s",__func__);
    }

    -(void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err{
    if (err) {
    NSLog(@"连接失败");
    }else{
    NSLog(@"正常断开");
    }

    if ([sock.userData isEqualToString:[NSString stringWithFormat:@"%d",SOCKET_CONNECT_SERVER]])
    {
    //服务器掉线 重新连接
    [self connectToServerWithCommand:@"battery"];
    }else
    {
    return;
    }
    }

    -(void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag {
    NSLog(@"数据发送成功:%s",__func__);
    //发送完数据手动读取,-1不设置超时
    [sock readDataWithTimeout:-1 tag:tag];
    }

    -(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
    NSString *receiverStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"读取数据:%s %@",__func__,receiverStr);
    }

    摘自:https://www.jianshu.com/p/8e599ca5dfe8

    收起阅读 »

    iOS 接入WebSocket

    WebSocket是什么WebSocket协议是 基于TCP 的一种网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。WebSocket基本原理帧协议:0 ...
    继续阅读 »

    WebSocket是什么

    WebSocket协议是 基于TCP 的一种网络协议。
    它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。


    WebSocket基本原理

    帧协议:

    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    +-+-+-+-+-------+-+-------------+-------------------------------+
    |F|R|R|R| opcode|M| Payload len | Extended payload length |
    |I|S|S|S| (4) |A| (7) | (16/64) |
    |N|V|V|V| |S| | (if payload len==126/127) |
    | |1|2|3| |K| | |
    +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
    | Extended payload length continued, if payload len == 127 |
    + - - - - - - - - - - - - - - - +-------------------------------+
    | |Masking-key, if MASK set to 1 |
    +-------------------------------+-------------------------------+
    | Masking-key (continued) | Payload Data |
    +-------------------------------- - - - - - - - - - - - - - - - +
    : Payload Data continued ... :
    + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
    | Payload Data continued ... |
    +---------------------------------------------------------------+

    • fin(1 bit):指示该帧是否构成该消息的最终帧。大多数情况下,消息适合于一个单一的帧,这一点总是默认设置的。实验表明,Firefox 在 32K 之后创建了第二个帧。
    • rsv1,rsv2,rsv3(1 bit each):必须为0,除非扩展里协商定义了非零值的含义。如果收到一个非零值,并且协商的扩展中没有一个定义这个非零值的含义,那么接收端必须抛出失败连接。

    • opcode(4bits):展示了帧表示什么。以下值目前正在使用:
      0x00:这个帧继续前面的有效载荷。
      0x01:此帧包含文本数据。
      0x02:这个帧包含二进制数据。
      0x08:这个帧终止连接。
      0x09:这个帧是一个 ping 。
      0x0a:这个帧是一个 pong 。
      (正如你所看到的,有足够的值未被使用,它们已被保留供将来使用)。
    • payload:最可能被掩盖的实际数据。它的长度是 payload_len 的长度。
    • masking-key(32 bits):从客户端发送到服务器的所有帧都被帧中包含的 32 位值掩盖。
    • 0-125 表示有效载荷的长度。 126 表示以下两个字节表示长度,127 表示接下来的 8 个字节表示长度。所以有效负载的长度在 〜7bit,16bit 和 64bit 括号内。
    • payload_len(7 bits):有效载荷的长度。 WebSocket 的帧有以下长度括号:
    • mask(1 bit):指示连接是否掩盖。就目前而言,从客户端到服务器的每条消息都必须掩盖,如果规范没有掩盖,规范就会终止连接。

    其他特点包括:
    (1)建立在 TCP 协议之上,服务器端的实现比较容易。
    (2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

    (3)数据格式比较轻量,性能开销小,通信高效。
    (4)可以发送文本,也可以发送二进制数据。

    (5)没有同源限制,客户端可以与任意服务器通信。
    (6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

    WebSocket常见的使用场景

    要求服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息。(也可以采用HTTP/2)

    iOS端实现WebSocket连接的参考方案

    SocketRocket

    SocketRocket是facebook封装的websocket开源库,采用纯Objective-C编写。
    使用者需要自己实现心跳机制,以及适配断网重连等情况。

    SocketIO

    SocketIO将WebSocket、AJAX和其它的通信方式全部封装成了统一的通信接口,也就是说,我们在使用SocketIO时,不用担心兼容问题,底层会自动选用最佳的通信方式。因此说,WebSocket是SocketIO的一个子集。

    另外,如果后端采用的是原生WebSocket,不建议大家使用SocketIO。
    因为SocketIO定制了专有的协议,并不是纯粹的WebSocket,可能会遭遇适配问题。
    不过,SocketIO的API极其易用!!!

    Starscream

    采用Swift编写,不过笔者暂时还没有用过,不发表任何评论。

    iOS端利用SocketRocket实现WebSocket连接

    示例代码如下,欢迎指正:

    import Foundation
    import SocketRocket
    import Alamofire

    /// Websocket连接中的通知
    let FSWebSocketConnectingNotification = NSNotification.Name.init("FSWebSocketConnectingNotification")
    /// Websocket连接成功的通知
    let FSWebSocketDidOpenNotification = NSNotification.Name.init("FSWebSocketDidOpenNotification")
    /// Websocket连接收到新消息的通知
    let FSWebSocketDidReceiveMessageNotification = NSNotification.Name.init("FSWebSocketDidReceiveMessageNotification")
    /// Websocket连接失败的通知
    let FSWebSocketFailWithErrorNotification = NSNotification.Name.init("FSWebSocketFailWithErrorNotification")
    /// Websocket连接已关闭的通知
    let FSWebSocketDidCloseNotification = NSNotification.Name.init("FSWebSocketDidCloseNotification")

    /// websocket连接地址,请输入有效的websocket地址!
    let WSAddr = "ws://host:port/ws"

    /// 心跳包发送间隔,3分钟
    let PingDuration = 180

    /// Websocket连接对象
    class FSWebsocket: NSObject,SRWebSocketDelegate {

    static let `default` = FSWebsocket.init()
    /// 是否在断开连接后自动重连
    var autoReconnect = true

    private var reachabilityManager = NetworkReachabilityManager.init(host: "www.baidu.com")
    /// websocket连接地址
    private var addr = WSAddr
    /// websocket连接
    private var ws:SRWebSocket?
    /// 心跳包定时发送计时器
    private var heartbeatTimer:Timer?
    /// 重连计数器
    private var reconnectCount = 0

    override init() {
    super.init()

    NotificationCenter.default.addObserver(self, selector: #selector(appWillEnterForeground), name: NSNotification.Name.UIApplicationWillEnterForeground, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(appDidEnterBackground), name: NSNotification.Name.UIApplicationDidEnterBackground, object: nil)

    // 切换网络自动重连
    reachabilityManager?.listener = { [weak self] (networkReachabilityStatus) in
    // 正在连接中或已经连接成功,不需要重连
    if self?.ws?.readyState == .CONNECTING || self?.ws?.readyState == .OPEN{
    return
    }
    // 设置自动重连才会进行自动重连
    if let s = self, s.autoReconnect{
    self?.reconnect()
    }
    }
    reachabilityManager?.startListening()
    }
    deinit {
    NotificationCenter.default.removeObserver(self)
    }

    @objc func appDidEnterBackground(){

    close()
    }
    @objc func appWillEnterForeground(){

    reconnect()
    }

    // MARK: - Send Message

    @discardableResult
    func sendJSON(_ json:[AnyHashable : Any]) -> NSError?{
    let jsonData = try? JSONSerialization.data(withJSONObject: json, options: JSONSerialization.WritingOptions.prettyPrinted)
    return self.sendData(jsonData)
    }

    @discardableResult
    func sendData(_ data:Data?) -> NSError?{

    guard ws?.readyState == SRReadyState.OPEN else{
    return NSError.init(domain: "FSWebsocket", code: SRStatusCodeGoingAway.rawValue, userInfo: nil)
    }
    ws?.send(data)
    return nil
    }

    // MARK: - Connection Management

    /// 连接websocket服务器
    func open(){

    let url = URL(string: self.addr)
    assert(url != nil)

    self.ws = SRWebSocket.init(url: url!)
    ws?.delegate = self
    ws?.open()
    NotificationCenter.default.post(name: FSWebSocketConnectingNotification, object: self)
    }

    /// 重新连接
    private func reconnect(){

    if !autoReconnect{
    return
    }
    if reconnectCount > 64 {
    return
    }

    let seconds = DispatchTime.now().uptimeNanoseconds + UInt64(reconnectCount) * NSEC_PER_SEC
    let time = DispatchTime.init(uptimeNanoseconds: seconds)
    DispatchQueue.main.asyncAfter(deadline: time) {
    self.ws?.close()
    self.ws = nil

    self.open()
    }

    if reconnectCount == 0 {
    reconnectCount = 1
    }
    reconnectCount *= 2
    }

    /// 关闭连接
    func close(){

    self.destroyHeartbeat()

    self.ws?.close()
    self.ws = nil

    resetConnectCount()
    }

    /// 发送心跳包
    @objc func heartbeat(){

    guard ws?.readyState == SRReadyState.OPEN else{
    return
    }
    self.ws?.sendPing(nil)
    }
    /// 启动心跳
    private func startHeartbeat(){

    let timer = Timer.init(timeInterval: TimeInterval(PingDuration), target: self, selector: #selector(heartbeat), userInfo: nil, repeats: true)
    self.heartbeatTimer = timer
    RunLoop.current.add(timer, forMode: RunLoopMode.commonModes)
    }
    /// 停止心跳
    private func destroyHeartbeat(){

    self.heartbeatTimer?.invalidate()
    self.heartbeatTimer = nil
    }
    /// 重置重连尝试次数
    private func resetConnectCount(){

    reconnectCount = 0
    }


    // MARK: - SRWebSocketDelegate

    func webSocketDidOpen(_ webSocket: SRWebSocket!) {

    self.resetConnectCount()
    self.startHeartbeat()
    NotificationCenter.default.post(name: FSWebSocketDidOpenNotification, object: self)
    }

    func webSocket(_ webSocket: SRWebSocket!, didReceiveMessage message: Any!) {

    guard let msgString = message as? NSString else{
    return
    }
    guard let msgData = msgString.data(using: String.Encoding.utf8.rawValue) else{
    return
    }
    // 如果传输的是JSON数据,可以使用JSONSerialization将JSON字符串转换为字典
    guard let msgJSON = try? JSONSerialization.jsonObject(with: msgData, options: JSONSerialization.ReadingOptions.allowFragments) else{
    return
    }
    NotificationCenter.default.post(name: FSWebSocketDidReceiveMessageNotification, object: self, userInfo: ["message":msgJSON])
    }

    func webSocket(_ webSocket: SRWebSocket!, didFailWithError error: Error!) {

    NotificationCenter.default.post(name: FSWebSocketFailWithErrorNotification, object: self, userInfo: ["error": error])
    // 连接失败:
    if (error as NSError).code == 50{
    // 断网,不重连
    return
    }
    // 当前页面不需要使用websocket,不重连
    // 重连次数超过限制,不重连
    reconnect()
    }

    func webSocket(_ webSocket: SRWebSocket!, didReceivePong pongPayload: Data!) {
    // 心跳包响应回调
    }

    func webSocket(_ webSocket: SRWebSocket!, didCloseWithCode code: Int, reason: String!, wasClean: Bool) {

    // 连接被关闭
    NotificationCenter.default.post(name: FSWebSocketDidCloseNotification, object: self, userInfo: ["code": code, "reason": reason, "wasClean": wasClean])

    close()
    }
    }



    摘自链接:https://www.jianshu.com/p/934c0d79f75e

    收起阅读 »

    【性能优化】关键性能指标及测量标准

    前言随着时代的发展,业内曾经提出过很多性能优化相关的标准和规范,从最初2007年雅虎提出34条军规,到2020年google开始推广Web Vitals,优化技术已经有将近20多年的发展历史,如下图所示:发展过程中产生了许多的性能指标、测量工具、优化手段等等,...
    继续阅读 »

    前言

    随着时代的发展,业内曾经提出过很多性能优化相关的标准和规范,从最初2007年雅虎提出34条军规,到2020年google开始推广Web Vitals,优化技术已经有将近20多年的发展历史,如下图所示:


    发展过程中产生了许多的性能指标、测量工具、优化手段等等,本文主要讲述关键性能指标及测量标准。


    性能指标,顾名思义,就是性能优化过程中参考的一些标准。


    进行性能优化,指标就是我们的一个抓手,首先你就要确定它的指标,然后才能根据指标去采取措施,否则就会像无头苍蝇一样乱撞,没有执行目标。

    什么样的指标值得我们关注?

    Web Vitals

    Google在2020年推出了一个名为Web Vitals的新概念,着重评估一组页面访问体验钟的一部分关键指标,目的在于简化网络性能。每个指标代表页面体验的一个关键方面:加载、交互和视觉稳定性



    Web Vitals在感知的性能,交互性和令人愉悦的方面,可作为用户体验的简化基准。在考虑页面性能时,应该首先关注这一套较小的指标。


    此外,Web Vitals代表了访问者在访问您的页面时首先在其视口中看到的内容,即首屏内容。他们首先看到的内容最终会影响他们对页面性能的看法。


    首先,专注于这三个指标,可以使您获得可感知的和实际的性能可观的收益,然后再深入研究其他优化。


    加载


    所谓加载,就是进入页面时,页面内容的载入过程。比如,当你打开一些网站时,你会发现,有的网站首页上的文字、图片出现很缓慢,而有的则很快,这个内容出现的过程就是加载。加载缓慢严重消耗用户的耐心,会让用户离开页面。


    这里我们拿淘宝首页的network信息观察一些指标:


    如图所示,我们可以看到下方显示网页加载一共有201次请求(requests)、3.3MB资源量(resources),DOM完成的加载时间92ms(DOMContentLoaded)、总资源加载时间479ms(Load),这对一个电商网站来说已经很好了。


    瀑布图


    再来看瀑布图,nextwork中加载资源的列表右侧的Waterfall一栏显示的就是瀑布图,它可以非常直观的将网站的资源加载用自上而下的方式表达出来。我们可以从横向和纵向两个方向来解读它。


    横向来看是具体资源的加载,如下图所示:


    当鼠标悬浮在具体的色块上,我们可以看到具体的指标详情。如下图所示,资源下载之前经历了0.46ms的排队(Queueing),DNS查找24.79ms(DNS Lookup)、建立TCP连接(Initial connection)、还有https的网站SSL证书验证的时间37.04ms(SSL),最后发送请求再到资源返回也要经历110.71ms(TTFB)。

    下面我们再来纵向看瀑布图,主要看2点:



    1. 资源之间的联系:如果下载发生了阻塞,很多资源的下载就会是串行处理的。如果是并行的,就可以加快下载过程。

    2. 关键时间节点:我们可以看到图中有红蓝两个颜色的两根线,蓝色的是DOM完成的加载时间,红色是页面中所有声明的资源加载完成的时间。

    关键指标


    那么这么多指标,到底哪些是最值得我们关注的呢?下面我来总结一下:




    1. 白屏时间(FP,First Paint):也叫首次绘制时间,对于应用页面,首次出现视觉上不同于跳转之前内容的时间点,或者说是页面发生第一次绘制的时间点,它的标准时间是 300ms。如果白屏时间过长,用户会认为我们的页面不可用,或者可用性差。如果超过一定时间(如 1s),用户注意力就会转移到其他页面。




    2. 首屏时间(FCP,First Contentful Paint):也叫首次有内容绘制时间,对于所有的网页应用,这是一个非常重要的指标。它是指从浏览器输入地址并回车后,到首屏内容渲染完毕的时间。这期间不需要滚动鼠标或者下拉页面,否则无效。也就是说,它是浏览器完成渲染DOM中第一部分内容(可能是文本、图像或其它任何元素)的时间点,此时用户应该在视觉上有直观的感受。




    3. 首次有意义绘制(FMP,First Meaningful Paint):指页面关键元素的渲染时间。这个概念并没有标准化定义,因为关键元素可以由开发者自行定义究竟什么是“有意义”的内容,只有开发者或者产品经理自己了解。




    4. 速度指数(Speed Index):指的是网页以多快的速度展示内容,标准时间是4s




    5. 总下载时间(Load):页面所有资源加载完成所需要的时间。一般可以统计 window.onload,得到同步加载的资源全部加载完的耗时。如果页面中存在较多异步渲染,那么可以将异步渲染全部完成的时间作为总下载时间。




    6. TTFB(Time To First Byte):是指网络请求被发起到从服务器接收到地一个字节的这段时间。其中包含了TCP连接时间、发送HTTP请求时间和获得相应消息第一个字节的时间。


      TTFB这个参数比较重要,因为它可以给用户最直观的感受,如果TTFB很慢,说明资源一直没有返回,增加白屏时间,如果TTFB很快,资源回来之后就可以进行快速的解析和渲染。


      那么影响TTFB的因素有哪些?



      1. 后台处理能力,服务器响应到底有多快

      2. 资源请求的网络状况,网络是否有延迟

    首屏时间 vs 白屏时间


    首屏时间 = 白屏时间 + 渲染时间。在加载性能指标方面,相比于白屏时间,首屏时间更重要。为什么?


    从重要性角度看,打开页面后,第一眼看到的内容一般都非常关键,比如电商的头图、商品价格、购买按钮等。这些内容即便在最恶劣的网络环境下,我们也要确保用户能看得到。


    从体验完整性角度看,进入页面后先是白屏,随着第一个字符加载,到首屏内容显示结束,我们才会认为加载完毕,用户可以使用了。白屏加载完成后,仅仅意味着页面内容开始加载,但我们还是没法完成诸如下单购买等实际操作,首屏时间结束后则可以。


    DOMContentLoaded和Load事件的区别


    其实从这两个事件的命名就能体会到,DOMContentLoaded 指的是文档中 DOM 加载内容加载完毕的时间,也就是说 HTML 结构已经是完整的了。但是我们知道,很多页面都包含图片、特殊字体、视频、音频等其他资源,由于这些资源由网络请求获取,需要额外的网络请求,因此DOM内容如加载完毕时,这些资源还没有请求或渲染完成。当页面上所有资源加载完成后,Load 事件才会被触发。


    因此,在时间线上,Load 事件往往会落后于 DOMContentLoaded 事件

    交互/响应


    所谓交互,就是用户点击网站或 App 的某个功能,页面给出的回应,也就是浏览器的响应时间。比如我们点击了一个“点赞”按钮,立刻给出了点赞数加一的展示,这就是交互体验好,反之如果很长时间都没回应,这就是交互体验不好。


    关于交互指标,有的公司用 FID 指标 (First Input Delay,首次输入延迟), 指标必须尽量小于 100ms,如果过长会给人页面卡顿的感觉。还有的公司使用 PSI(Perceptual Speed Index,视觉变化率),衡量标准是小于20%。


    一般来说,主要包括以下几个指标:




    1. 交互动作的反馈时间:也叫用户可交互时间,就是用户可以与应用进行加护的时间,一般来讲,我们认为是 DOMReady 的时间,因为我们通常会在这时绑定事件操作。如果页面中设计交互的脚本没有下载完成,那么当然没有达到所谓的用户可交互时间。那么如何定义 DOMReady 时间呢?这里我推荐大家看司徒正美的文章《何谓DOMReady》。




    2. 刷新率(FPS,Frame Per Second):也叫帧率,标准的刷新率指标是60帧/s,它可以决定画面是否足够流畅。




    3. 异步请求的完成时间:所有的异步请求能在1s中内请求回来。




    关于帧率,我们可以用chorme Devtools来查看,打开控制台,点击快捷键command/ctrl+shift+P,弹出下面的弹窗,输入frame,点击FPS一栏,就会在页面左上角看到图2所示的监控台,显示网页交互过程中每一帧的绘制频率。



    不同帧率的体验



    • 帧率能够达到 50 ~ 60 FPS 的动画将会相当流畅,让人倍感舒适;

    • 帧率在 30 ~ 50 FPS 之间的动画,因各人敏感程度不同,舒适度因人而异;

    • 帧率在 30 FPS 以下的动画,让人感觉到明显的卡顿和不适感;

    • 帧率波动很大的动画,亦会使人感觉到卡顿


    现在网上很多关于浏览器reflow的文章都说给要少用offsetTop, offsetLeft 等获取布局信息的。因为这些属性需要触发一次浏览器的的Layout。也就是说在一帧内(16ms)会多了一次layout。如果Layout的次数太多,就会导致掉帧。


    视觉稳定性


    视觉稳定性指标CLS(Cumulative Layout Shift),也就是布局偏移量,它是指页面从一帧切换到另外一帧时,视线中不稳定元素的偏移情况。


    比如,你想要购买的商品正在参加抢购活动,而且时间快要到了。在你正要点击页面链接购买的时候,原来的位置插入了一条 9.9 元包邮的商品广告。结果会怎样?你点成了那个广告商品。如果等你再返回购买的时候,你心仪商品的抢购活动结束了,你是不是很气?所以,CLS也非常重要。


    一个好的CLS分数是75%以上的用户小于0.1,如图所示:




    布局偏移的具体内容


    布局偏移是由 Layout Instability API 定义的。这个API会在任意时间上报 layout-shift 的条目,当一个可见元素在两帧之间,改变了它的起始位置(默认的 writing mode 下指的是top和left属性)。这些元素被当成不稳定元素。


    需要注意的是,布局偏移只发生在已经存在的元素改变起始位置的时候。如果一个新的元素被添加到dom上,或者已存在的元素改变它的尺寸,除非改变了其他元素的起始位置,否则都不算布局偏移。


    布局偏移主要包含以下几项:




    1. 布局偏移分数:布局偏移的分数是两个度量的乘积:影响分数(impact fraction)和距离分数(distance fraction)。如果是一个很大的元素移动了较小的距离,实际影响并不大,所以分数需要依赖两个度量。




    2. 影响分数:影响分数测试的是两帧之间,不稳定元素在视图上的影响范围。




    3. 距离分数:距离分数测试的是两帧之间,不稳定元素在视图上移动的距离(水平和纵向取最大值)。如果有多个不稳定元素,也是取其中最大的一个。




    4. 动画和过渡:动画和过渡,如果做得好,对用户而言是一个不错的更新内容的方式,这样不会给用户“惊喜”。突然出现的或者预料之外的内容,会给用户一个很差的体验。但如果是一个动画或者过渡,用户可以很清楚的知道发生了什么,在状态变化的时候可以很好的引导用户。


      CSS中的 transform 属性可以让你在使用动画的时候不会产生布局偏移。



      • transform:scale() 来替换 widthheight 属性

      • transform:translate() 来替换 top, left, bottom, right 属性




    CLS是平时开发很少关注的点,页面视觉稳定性对很多web开发而言,可能没有加载性能那么关注度高,但对用户而言,这确实是很困扰的一点。平时开发中,尽可能的提醒自己,不管是产品交互图出来之后,或者是UI的视觉稿出来之后,如果出现了布局偏移的情况,都可以提出这方面的意见。开发过程中也尽可能的遵循上面提到的一些优化点,给用户一个稳定的视觉体验。


    RAIL测量模型


    RAIL模型是2015年google提出的一个可以量化的测量标准,通过RAIL模型可以指导性能优化的目标,让良好的用户体验成为可能。




    1. Response 响应:是指用户操作网页的时候浏览器给到用户的反馈时间,其中处理事件应在50ms以内完成。


      为什么是50ms?谷歌向向用户发起调研,将用户的反馈分成了几个组,经过研究得出用户能接受的反馈时间是100ms。


      那么为什么我们要设置在50ms以内,因为100ms是用户输入到反馈的时间,但是浏览器处理反馈也需要时间,所以留给开发者优化处理事件的时间在50ms以内。如下图所示:


  • Animation - 页面中动画特效的流畅度,达到每10ms产生一帧。


    根据研究得出,动画要达到60sps,即每秒60帧给人眼的感觉是流畅的,每一帧大概在16ms,去除浏览器绘制动画的6ms,开发者要保证每10ms产生一帧。


    在这16ms内浏览器要完成的工作有:



    • 脚本执行(JavaScript):脚本造成了需要重绘的改动,比如增删 DOM、请求动画等

    • 样式计算(CSS Object Model):级联地生成每个节点的生效样式。

    • 布局(Layout):计算布局,执行渲染算法

    • 重绘(Paint):各层分别进行绘制(比如 3D 动画)

    • 合成(Composite):将位图发送给合成线程。




  • Idle空闲 - 浏览器有足够的空闲时间,与响应想呼应。尽可能最大化空闲时间,不能让事件处理时间太长,超过50ms。


    例如延迟加载可以用空闲时间去加载。但是如果需要前端做业务计算,就是不合理的。




  • Load - 网络加载时间,在5s内完成内容加载并可以交互。首先加载-解析-渲染的时间在5s,其次网络环境差的情况下,加载也会受到影响。

  • 总结

    至此,性能优化的指标我就介绍完了,现将关键指标总结如下:



    1. 性能优化的三个方向:加载、交互、视觉稳定性

    2. 加载的关键指标有:TTFB(请求等待时间)、FP(白屏时间)、FCP(首屏时间)、Speed Index(4s)

    3. 交互的关键指标:用户可交互时间、帧率(FPS)、异步请求完成时间

    4. 交互稳定性(CLS):布局偏移量中,布局偏移分数 = 影响分数 x 距离分数

    5. RAIL测量模型关注点:响应时间50ms、动画10ms/帧、浏览器空闲时间<50ms、网络加载时间5s

    链接:https://juejin.cn/post/6956583036133572639

    收起阅读 »

    前端的你还不会优化你的图片资源?来看这一篇就够了!

    优质的图片可以有效吸引用户,给用户良好的体验,所以随着互联网的发展,越来越多的产品开始使用图片来提升产品体验。相较于页面其他元素,图片的体积不容忽视。下图是截止 2019 年 6 月 HTTP Archive[1] 上统计的网站上各类资源加...
    继续阅读 »

    优质的图片可以有效吸引用户,给用户良好的体验,所以随着互联网的发展,越来越多的产品开始使用图片来提升产品体验。相较于页面其他元素,图片的体积不容忽视。下图是截止 2019 年 6 月 HTTP Archive[1] 上统计的网站上各类资源加载的体积:


    可以看到,图片占据了半壁江山。同样,在一篇 2018 年的文章中,也提到了图片在网站中体量的平均占比已经超过了 50%[2]。然而,随着平均加载图片总字节数的增加,图片的请求数却再减少,这也说明网站使用的图片质量和大小正在不断提高。


    所以,如果单纯从加载的字节数这个维度来看性能优化,那么很多时候,优化图片带来的流量收益要远高于优化 JavaScript 脚本和 CSS 样式文件。下面我们就来看看,如何优化图片资源。


    1. 优化请求数

    1.1. 雪碧图


    图片可以合并么?当然。最为常用的图片合并场景就是雪碧图(Sprite)[3]


    在网站上通常会有很多小的图标,不经优化的话,最直接的方式就是将这些小图标保存为一个个独立的图片文件,然后通过 CSS 将对应元素的背景图片设置为对应的图标图片。这么做的一个重要问题在于,页面加载时可能会同时请求非常多的小图标图片,这就会受到浏览器并发 HTTP 请求数的限制。我见过一个没有使用雪碧图的页面,首页加载时需要发送 20+ 请求来加载图标。将图标合并为一张大图可以实现「20+ → 1」的巨大缩减。


    雪碧图的核心原理在于设置不同的背景偏移量,大致包含两点:



    • 不同的图标元素都会将 background-url 设置为合并后的雪碧图的 uri;

    • 不同的图标通过设置对应的 background-position 来展示大图中对应的图标部分。


    你可以用 Photoshop 这类工具自己制作雪碧图。当然比较推荐的还是将雪碧图的生成集成到前端自动化构建工具中,例如在 webpack 中使用 webpack-spritesmith,或者在 gulp 中使用 gulp.spritesmith。它们两者都是基于于 spritesmith 这个库,你也可以自己将这个库集成到你喜欢的构建工具中。

    1.2. 懒加载


    我们知道,一般来说我们访问一个页面,浏览器加载的整个页面其实是要比可视区域大很多的,也是什么我们会提出“首屏”的概念。这就导致其实很多图片是不在首屏中的,如果我们都加载的话,相当于是加载了用户不一定会看到图片。而图片体积一般都不小,这显然是一种流量的浪费。这种场景在一些带图片的长列表或者配图的博客中经常会遇到。


    解决的核心思路就是图片懒加载 —— 尽量只加载用户正在浏览或者即将会浏览到的图片。实现上来说最简单的就是通过监听页面滚动,判断图片是否进入视野,从而真正去加载图片:

    function loadIfNeeded($img) {
    const bounding = $img..getBoundingClientRect();
    if (
    getComputedStyle($img).display !== 'none'
    && bounding.top <= window.innerHeight
    && bounding.bottom >= 0
    ) {
    $img.src = $img.dataset.src;
    $img.classList.remove('lazy');
    }
    }

    // 这里使用了 throttle,你可以实现自己的 throttle,也可以使用 lodash
    const lazy = throttle(function () {
    const $imgList = document.querySelectorAll('.lazy');
    if ($imgList.length === 0) {
    document.removeEventListener('scroll', lazy);
    window.removeEventListener('resize', lazy);
    window.removeEventListener('orientationchange', lazy);
    return;
    }
    $imgList.forEach(loadIfNeeded);
    }, 200);

    document.addEventListener('scroll', lazy);
    window.addEventListener('resize', lazy);
    window.addEventListener('orientationchange', lazy);

    对于页面上的元素只需要将原本的 src 值设置到 data-src 中即可,而 src 可以设置为一个统一的占位图。注意,由于页面滚动、缩放和横竖方向(移动端)都可能会改变可视区域,因此添加了三个监听。


    当然,这是最传统的方法,现代浏览器还提供了一个更先进的 Intersection Observer API[4] 来做这个事,它可以通过更高效的方式来监听元素是否进入视口。考虑兼容性问题,在生产环境中建议使用对应的 polyfill


    如果想使用懒加载,还可以借助一些已有的工具库,例如 aFarkas/lazysizesverlok/lazyloadtuupola/lazyload 等。


    在使用懒加载时也有一些注意点:



    • 首屏可以不需要懒加载,对首屏图片也使用懒加载会延迟图片的展示。

    • 设置合理的占位图,避免图片加载后的页面“抖动”。

    • 虽然目前基本所有用户都不会禁用 JavaScript,但还是建议做一些 JavaScript 不可用时的 backup。


    对于占位图这块可以再补充一点。为了更好的用户体验,我们可以使用一个基于原图生成的体积小、清晰度低的图片作为占位图。这样一来不会增加太大的体积,二来会有很好的用户体验。LQIP (Low Quality Image Placeholders)[5] 就是这种技术。目前也已经有了 LQIPSQIP(SVG-based LQIP) 的自动化工具可以直接使用。


    如果你想了解更多关于图片懒加载的内容,这里有一篇更详尽的图片懒加载指南[6]

    1.3. CSS 中的图片懒加载

    除了对于 <img> 元素的图片进行来加载,在 CSS 中使用的图片一样可以懒加载,最常见的场景就是 background-url 

    .login {
    background-url: url(/static/img/login.png);
    }

    对于上面这个样式规则,如果不应用到具体的元素,浏览器不会去下载该图片。所以你可以通过切换 className 的方式,放心得进行 CSS 中图片的懒加载。


    1.4. 内联 base64


    还有一种方式是将图片转为 base64 字符串,并将其内联到页面中返回,即将原 url 的值替换为 base64。这样,当浏览器解析到这个的图片 url 时,就不会去请求并下载图片,直接解析 base64 字符串即可。


    但是这种方式的一个缺点在于相同的图片,相比使用二进制,变成 base64 后体积会增大 33%。而全部内联进页面后,也意味着原本可能并行加载的图片信息,都会被放在页面请求中(像当于是串行了)。同时这种方式也不利于复用独立的文件缓存。所以,使用 base64 需要权衡,常用于首屏加载 CRP 或者骨架图上的一些小图标。


    2. 减小图片大小

    2.1. 使用合适的图片格式


    使用合适的图片格式不仅能帮助你减少不必要的请求流量,同时还可能提供更好的图片体验。


    图片格式是一个比较大的话题,选择合适的格式[7]有利于性能优化。这里我们简单总结一些。


    1) 使用 WebP:


    考虑在网站上使用 WebP 格式[8]。在有损与无损压缩上,它的表现都会优于传统(JPEG/PNG)格式。WebP 无损压缩比 PNG 的体积小 26%,webP 的有损压缩比同质量的 JPEG 格式体积小 25-34%。同时 WebP 也支持透明度。下面提供了一种兼容性较好的写法。

    <picture>
    <source type="image/webp" srcset="/static/img/perf.webp">
    <source type="image/jpeg" srcset="/static/img/perf.jpg">
    <img src="/static/img/perf.jpg">
    </picture>

    2) 使用 SVG 应对矢量图场景:


    在一些需要缩放与高保真的情况,或者用作图标的场景下,使用 SVG 这种矢量图非常不错。有时使用 SVG 格式会比相同的 PNG 或 JPEG 更小。


    3) 使用 video 替代 GIF:


    兼容性允许的情况下考虑,可以在想要动图效果时使用视频,通过静音(muted)的 video 来代替 GIF。相同的效果下,GIF 比视频(MPEG-4)大 5~20 倍Smashing Magazine 上有篇文章[9]详细介绍使用方式。


    4) 渐进式 JPEG:


    基线 JPEG (baseline JPEG) 会从上往下逐步呈现,类似下面这种:


    而另一种渐进式 JPEG (progressive JPEG)[10] 则会从模糊到逐渐清晰,使人的感受上会更加平滑。


    不过渐进式 JPEG 的解码速度会慢于基线 JPEG,所以还是需要综合考虑 CPU、网络等情况,在实际的用户体验之上做权衡。


    2.2. 图片质量的权衡


    图片的压缩一般可以分为有损压缩(lossy compression)和无损压缩(lossless compression)。顾名思义,有损压缩下,会损失一定的图片质量,无损压缩则能够在保证图片质量的前提下压缩数据大小。不过,无损压缩一般可以带来更可观的体积缩减。在使用有损压缩时,一般我们可以指定一个 0-100 的压缩质量。在大多数情况下,相较于 100 质量系数的压缩,80~85 的质量系数可以带来 30~40% 的大小缩减,同时对图片效果影响较小,即人眼不易分辨出质量效果的差异。



    处理图片压缩可以使用 imagemin 这样的工具,也可以进一步将它集成至 webpackGulpGrunt 这样的自动化工具中。


    2.3. 使用合适的大小和分辨率


    由于移动端的发展,屏幕尺寸更加多样化了。同一套设计在不同尺寸、像素比的屏幕上可能需要不同像素大小的图片来保证良好的展示效果;此外,响应式设计也会对不同屏幕上最佳的图片尺寸有不同的要求。


    以往我们可能会在 1280px 宽度的屏幕上和 640px 宽度的屏幕上都使用一张 400px 的图,但很可能在 640px 上我们只需要 200px 大小的图片。另一方面,对于如今盛行的“2 倍屏”、“3 倍屏”也需要使用不同像素大小的资源。


    好在 HTML5 在 <img> 元素上为我们提供了 srcsetsizes 属性,可以让浏览器根据屏幕信息选择需要展示的图片。

    <img srcset="small.jpg 480w, large.jpg 1080w" sizes="50w" src="large.jpg" >

    2.4. 删除冗余的图片信息


    你也许不知道,很多图片含有一些非“视觉化”的元信息(metadata),带上它们可会导致体积增大与安全风险[12]。元信息包括图片的 DPI、相机品牌、拍摄时的 GPS 等,可能导致 JPEG 图片大小增加 15%。同时,其中的一些隐私信息也可能会带来安全风险。


    所以如果不需要的情况下,可以使用像 imageOptim 这样的工具来移除隐私与非关键的元信息。


    2.5 SVG 压缩


    在 2.1. 中提到,合适的场景下可以使用 SVG。针对 SVG 我们也可以进行一些压缩。压缩包括了两个方面:


    首先,与图片不同,图片是二进制形式的文件,而 SVG 作为一种 XML 文本,同样是适合使用 gzip 压缩的。


    其次,SVG 本身的信息、数据是可以压缩的,例如用相比用 <path> 画一个椭圆,直接使用 <ellipse> 可以节省文本长度。关于信息的“压缩”还有更多可以优化的点[13]SVGGO 是一个可以集成到我们构建流中的 NodeJS 工具,它能帮助我们进行 SVG 的优化。当然你也可以使用它提供的 Web 服务


    3. 缓存

    与其他静态资源类似,我们仍然可以使用各类缓存策略来加速资源的加载。




    图片作为现代 Web 应用的重要部分,在资源占用上同样也不可忽视。可以发现,在上面提及的各类优化措施中,同时附带了相应的工具或类库。平时我们主要的精力会放在 CSS 与 JavaScript 的优化上,因此在图片优化上可能概念较为薄弱,自动化程度较低。如果你希望更好得去贯彻图片的相关优化,非常建议将自动化工具引入到构建流程中。


    除了上述的一些工具,这里再介绍两个非常好用的图片处理的自动化工具:SharpJimp


    好了,我们的图片优化之旅就暂时到这了,下面就是字体资源了。


    链接:https://juejin.cn/post/6962800616259190792



    收起阅读 »

    仅使用CSS就可以提高页面渲染速度的4个技巧

    用户喜欢快速的网络应用,他们希望页面加载速度快,功能流畅。如果在滚动时有破损的动画或滞后,用户很有可能会离开你的网站。作为一名开发者,你可以做很多事情来改善用户体验。本文将重点介绍4个可以用来提高页面渲染速度的CSS技巧。1. Content-visibili...
    继续阅读 »

    用户喜欢快速的网络应用,他们希望页面加载速度快,功能流畅。如果在滚动时有破损的动画或滞后,用户很有可能会离开你的网站。作为一名开发者,你可以做很多事情来改善用户体验。本文将重点介绍4个可以用来提高页面渲染速度的CSS技巧。

    1. Content-visibility


    一般来说,大多数Web应用都有复杂的UI元素,它的扩展范围超出了用户在浏览器视图中看到的内容。在这种情况下,我们可以使用内容可见性( content-visibility )来跳过屏幕外内容的渲染。如果你有大量的离屏内容,这将大大减少页面渲染时间。


    这个功能是最新增加的功能之一,也是对提高渲染性能影响最大的功能之一。虽然 content-visibility 接受几个值,但我们可以在元素上使用 content-visibility: auto; 来获得直接的性能提升。


    让我们考虑一下下面的页面,其中包含许多不同信息的卡片。虽然大约有12张卡适合屏幕,但列表中大约有375张卡。正如你所看到的,浏览器用了1037ms来渲染这个页面。


    下一步,您可以向所有卡添加 content-visibility 。

    在这个例子中,在页面中加入 content-visibility 后,渲染时间下降到150ms,这是6倍以上的性能提升。


    正如你所看到的,内容可见性是相当强大的,对提高页面渲染时间非常有用。根据我们目前所讨论的东西,你一定是把它当成了页面渲染的银弹。

    然而,有几个领域的内容可视性不佳。我想强调两点,供大家参考。



    • 此功能仍处于试验阶段。 截至目前,Firefox(PC和Android版本)、IE(我认为他们没有计划在IE中添加这个功能)和,Safari(Mac和iOS)不支持内容可见性。

    • 与滚动条行为有关的问题。 由于元素的初始渲染高度为0px,每当你向下滚动时,这些元素就会进入屏幕。实际内容会被渲染,元素的高度也会相应更新。这将使滚动条的行为以一种非预期的方式进行。

    为了解决滚动条的问题,你可以使用另一个叫做 contain-intrinsic-size 的 CSS 属性。它指定了一个元素的自然大小,因此,元素将以给定的高度而不是0px呈现。

    .element{
    content-visibility: auto;
    contain-intrinsic-size: 200px;
    }

    然而,在实验时,我注意到,即使使用 conta-intrinsic-size,如果我们有大量的元素, content-visibility 设置为 auto ,你仍然会有较小的滚动条问题。


    因此,我的建议是规划你的布局,将其分解成几个部分,然后在这些部分上使用内容可见性,以获得更好的滚动条行为。

    2. Will-change 属性

    浏览器上的动画并不是一件新鲜事。通常情况下,这些动画是和其他元素一起定期渲染的。不过,现在浏览器可以使用GPU来优化其中的一些动画操作。



    通过will-change CSS属性,我们可以表明元素将修改特定的属性,让浏览器事先进行必要的优化。



    下面发生的事情是,浏览器将为该元素创建一个单独的层。之后,它将该元素的渲染与其他优化一起委托给GPU。这将使动画更加流畅,因为GPU加速接管了动画的渲染。

    // In stylesheet
    .animating-element {
    will-change: opacity;
    }

    // In HTML
    <div class="animating-elememt">
    Animating Child elements
    </div>

    当在浏览器中渲染上述片段时,它将识别 will-change 属性并优化未来与不透明度相关的变化。



    根据Maximillian Laumeister所做的性能基准,可以看到他通过这个单行的改变获得了超过120FPS的渲染速度,而最初的渲染速度大概在50FPS。


    什么时候不是用will-change


    虽然 will-change 的目的是为了提高性能,但如果你滥用它,它也会降低Web应用的性能。




    • **使用 will-change 表示该元素在未来会发生变化。**因此,如果你试图将 will-change 和动画同时使用,它将不会给你带来优化。因此,建议在父元素上使用 will-change ,在子元素上使用动画。

    .my-class{
    will-change: opacity;
    }
    .child-class{
    transition: opacity 1s ease-in-out;
    }
    • 不要使用非动画元素。 当你在一个元素上使用 will-change 时,浏览器会尝试通过将元素移动到一个新的图层并将转换工作交给GPU来优化它。如果您没有任何要转换的内容,则会导致资源浪费。




    最后需要注意的是,建议在完成所有动画后,将元素的 will-change 删除。

    3.减少渲染阻止时间

    今天,许多Web应用必须满足多种形式的需求,包括PC、平板电脑和手机等。为了完成这种响应式的特性,我们必须根据媒体尺寸编写新的样式。当涉及页面渲染时,它无法启动渲染阶段,直到 CSS对象模型(CSSOM)已准备就绪。根据你的Web应用,你可能会有一个大的样式表来满足所有设备的形式因素。
    <link rel="stylesheet" href="styles.css">

    将其分解为多个样式表后:

    <!-- style.css contains only the minimal styles needed for the page rendering -->
    <link rel="stylesheet" href="styles.css" media="all" />
    <!-- Following stylesheets have only the styles necessary for the form factor -->
    <link rel="stylesheet" href="sm.css" media="(min-width: 20em)" />
    <link rel="stylesheet" href="md.css" media="(min-width: 64em)" />
    <link rel="stylesheet" href="lg.css" media="(min-width: 90em)" />
    <link rel="stylesheet" href="ex.css" media="(min-width: 120em)" />
    <link rel="stylesheet" href="print.css" media="print" />

    如您所见,根据样式因素分解样式表可以减少渲染阻止时间。

    4.避免@import包含多个样式表

    通过 @import,我们可以在另一个样式表中包含一个样式表。当我们在处理一个大型项目时,使用 @import 可以使代码更加简洁。



    关于 @import 的关键事实是,它是一个阻塞调用,因为它必须通过网络请求来获取文件,解析文件,并将其包含在样式表中。如果我们在样式表中嵌套了 @import,就会妨碍渲染性能。

    # style.css
    @import url("windows.css");
    # windows.css
    @import url("componenets.css");


    与使用 @import 相比,我们可以通过多个 link 来实现同样的功能,但性能要好得多,因为它允许我们并行加载样式表。

    总结

    除了我们在本文中讨论的4个方面,我们还有一些其他的方法可以使用CSS来提高网页的性能。CSS最近的一个特性: content-visibility,在未来的几年里看起来是如此的有前途,因为它给页面渲染带来了数倍的性能提升。



    最重要的是,我们不需要写一条JavaScript语句就能获得所有的性能。



    我相信你可以结合以上的一些功能,为终端用户构建性能更好的Web应用。希望这篇文章对你有用,如果你知道什么CSS技巧可以提高Web应用的性能,请在下面的评论中提及。谢谢大家。



    链接:https://juejin.cn/post/6911203296078692359

    收起阅读 »

    任意组合判断还在用Switch?位运算符了解一下~

    情景再现很多时候,当我们写程序都会有这样的情况,就是代码多选操作.例如下面的操作.比如有四个视图View(分别为A,B,C,D);当点击按钮a的时候,视图A,B背景色发生改变;当点击按钮b的时候,视图A,B,D背景色发生改变;当点击按钮c的时候,视图B,C,D...
    继续阅读 »

    情景再现

    很多时候,当我们写程序都会有这样的情况,就是代码多选操作.例如下面的操作.


    比如有四个视图View(分别为A,B,C,D);
    当点击按钮a的时候,视图A,B背景色发生改变;
    当点击按钮b的时候,视图A,B,D背景色发生改变;
    当点击按钮c的时候,视图B,C,D背景色发生改变;
    后续开发中可能有很多按钮和不同的组合形式.

    这时候你会怎么办?

    第一种方案: 所有的按钮就响应一个方法,里面使用if else等模块来区分不同的按钮事件.
    思考问题: 后期如果增加一种按钮.你就需要增加一个if else,代码增加的同时,if else逻辑层级太多也不利于阅读.

    第二种方案: if else性能太低?我们就使用Switch.配合着枚举值或者按钮的Tag值来做区别判断,枚举值可以定义成每一种组合形式都是一个枚举值.不同按钮相同的组合形式进入同样的模块.
    思考问题: 虽然Switch使用break关键词相对于普通的if else有很大的性能提高,但是后期如果增加一种情况,仍然需要添加代码块.还是会增加代码量.

    这时候我们总结一下上面倒是需要干什么,以及出现的问题.

    需求: 组合是具有不确定性的,但是组合中的基本元素是确定的(A,B,C,D中的任意组合).

    问题: 普通的方式不管if else或者Switch都可能会需要罗列出所有的组合形式,代码量很大,不符合代码规范.阅读起来也是相当的困难.

    难道就没有更加优雅的方式来解决这个问题吗?这当然是有的,那就是我们的今天猪脚 位运算符.使用位运算符可以很好的帮助我们解决这一问题.但是在此之前我们需要先了解什么叫做位移运算符.

    位运算符


    按位与和按位或举个例子来看下.

    按位与

    1001
    &
    0101
    =0001

    按位或

    1001
    &
    0101
    =1101

    按位异或

    1001
    ^
    0101
    =1100

    我们了解了位运算符,我们该如何解决最开始的那种问题呢?我们接着看~

    解决问题

    → Demo传送门,对照着看

    解决这种问题我们会用到 << 和& 以及 | 这三种位运算符.

    首先定义一个ColorView视图,继承于UIView,然后在 .h 头文件定义枚举,并且ColorView持有枚举的属性.代码如下所示.

    #import <UIKit/UIKit.h>

    typedef enum : NSUInteger {
    ColorViewStyleA = 1<<0,
    ColorViewStyleB = 1<<1,
    ColorViewStyleC = 1<<2,
    ColorViewStyleD = 1<<3,
    } ColorViewStyle;

    @interface ColorView : UIView

    @property(nonatomic,assign)ColorViewStyle style;
    @property(nonatomic,copy)NSString *title;

    @end

    然后在ViewController导入ColorView,并且声明一个属性needChangeColorStyle.用于判断需要做出修改的视图.

    #import "ColorView.h"
    @interface ViewController ()

    @property(nonatomic,assign)ColorViewStyle needChangeColorStyle;

    @end

    然后在ViewController创建ColorView和Button,这里由于时间原因,我就简写了.主要要给每种视图设置一个枚举值,作为识别码.

    //创建ColorView
    for (int i = 0; i < 4; i++) {

    ColorView *colorView = [[ColorView alloc] initWithFrame:CGRectMake(viewWidth * i + distance *(i+1), 100, viewWidth, 100)];

    ............

    switch (i) {
    case 0:
    colorView.style = ColorViewStyleA;
    break;
    case 1:
    colorView.style = ColorViewStyleB;
    break;
    case 2:
    colorView.style = ColorViewStyleC;
    break;
    case 3:
    colorView.style = ColorViewStyleD;
    break;
    }
    ............
    }

    //创建按钮
    for (int i = 0; i < 4; i++) {
    ............
    }

    然后在按钮的点击方法 buttonAction 里面重置needChangeColorStyle的值,这里就需要使用到按位或进行枚举值的组合了.如下所示.

    - (void)buttonAction:(UIButton *)sender {

    //对 needChangeColorStyle 进行赋值,其实这步操作应该在一开始做的,这里是Demo,所以这么做了.
    //赋值过程中使用了按位或运算符.整合可响应的View类型.
    NSInteger tagIndex = sender.tag - 10000;
    switch (tagIndex) {
    case 0:
    _needChangeColorStyle = ColorViewStyleA|ColorViewStyleB|ColorViewStyleD;
    break;
    case 1:
    _needChangeColorStyle = ColorViewStyleB|ColorViewStyleC|ColorViewStyleD;
    break;
    case 2:
    _needChangeColorStyle = ColorViewStyleA|ColorViewStyleB;
    break;
    case 3:
    _needChangeColorStyle = ColorViewStyleD;
    break;
    }
    [self colorViewsChangAction];
    }

    最后在colorViewsChangAction方法中进行视图的操作选择.使用到了按位与运算.只要视图的style和_needChangeColorStyle有相交部分,那么两者按位与出来的数值一定是大于等于1的.这样就可以做包含操作了.代码日下所示.

    - (void)colorViewsChangAction {

    //遍历ColorView视图数组
    for (ColorView *colorView in self.colorViews) {
    //使用了按位与,查看两者是否具有相交部分.
    if (_needChangeColorStyle & colorView.style) {
    NSLog(@"%@ 做出了响应",colorView.title);
    colorView.backgroundColor = [UIColor redColor];
    } else {
    colorView.backgroundColor = [UIColor orangeColor];
    }
    }
    }

    这样我们就完成了使用位运算做任何组合判断的操作了,后期我们加一种按钮或者组合形式,只需要在 buttonAction 添加三行代码即可.其他都不用了,而且代码结构读起来非常的舒服.

    当然了,在iOS原生框架也是有这样的操作的,例如对于贝塞尔曲线的指定角进行切边操作.枚举值也是带有位移运算的,枚举值如下所示.这时候可以仍然可以使用按位或组合任意形式的角.

    typedef NS_OPTIONS(NSUInteger, UIRectCorner) {
    UIRectCornerTopLeft = 1 << 0,
    UIRectCornerTopRight = 1 << 1,
    UIRectCornerBottomLeft = 1 << 2,
    UIRectCornerBottomRight = 1 << 3,
    UIRectCornerAllCorners = ~0UL
    };

    总结

    位运算符还有很多用途,这是最简单的用途而已,在安卓那边的话,如果枚举有性能问题,可以使用定义常量的形式来实现该目的,整体上是一致的,好了就说到这里,如果有任何问题,欢迎批评指导,谢谢.最后再把Demo发一遍.

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

    收起阅读 »

    一种简单实用的 JS 动态加载方案

    背景 在做 Web 应用的时候,你或许遇到过这样的场景:为了实现一个使用率很低的功能,却引入了超大的第三方库,导致项目打包后的 JS bundle 体积急剧膨胀。 我们有一些具体的案例,例如: 产品要求在项目中增加一个导出数据为 Excel 文件的功能,这个功...
    继续阅读 »

    背景


    在做 Web 应用的时候,你或许遇到过这样的场景:为了实现一个使用率很低的功能,却引入了超大的第三方库,导致项目打包后的 JS bundle 体积急剧膨胀。


    我们有一些具体的案例,例如:


    产品要求在项目中增加一个导出数据为 Excel 文件的功能,这个功能其实只有管理员才能看到,而且最多一周才会使用一次,绝对属于低频操作。


    团队里的小伙伴为了实现这个功能,引入了 XLSX 这个库,JS bundle 体积因而增加了一倍,所有用户的体验都受到影响了。


    XLSX 用来做 Excel 相关的操作是不错的选择,但因为新增低频操作影响全部用户却不值得。


    除了导出 Excel 这种功能外,类似的场景还有使用 html2canvas 生成并下载海报,使用 fabric 动态生成图片等。


    针对这种情况,你觉得该如何优化呢?

    自动分包和动态加载


    机智如你很快就想到使用 JS 动态加载,如果熟悉 React,还知道可以使用 react-loadable 来解决。


    原理就是利用 React Code-Splitting,配合 Webpack 自动分包,动态加载。


    这种方案可以,React 也推荐这么做,但是对于引用独立的第三方库这样的场景,还有更简单的方案。

    更简单的方案


    这些第三方库往往都提供了 umd 格式的 min.js,我们动态加载这些 min.js 就可以了。比如 XLSX,引入其 min.js 文件之后,就可以通过 window.XLSX 来实现 Excel 相关的操作。


    此方案的优点有:



    • 与框架无关,不需要和 React 等框架或 Webpack 等工具绑定

    • 精细控制,React Code-Splitting 之类的方案只能到模块级别,想要在点击按钮后才动态加载较难实现

    具体实现

    我们重点需要实现一个 JS 动态加载器 AsyncLoader,代码如下:

    function initLoader() {
    // key 是对应 JS 执行后在 window 中添加的变量
    const jsUrls = {
    html2canvas: 'https://cdn.jsdelivr.net/npm/html2canvas@1.0.0-rc.7/dist/html2canvas.min.js',
    XLSX: 'https://cdn.jsdelivr.net/npm/xlsx@0.16.9/dist/xlsx.min.js',
    flvjs: 'https://cdn.jsdelivr.net/npm/flv.js@1.5.0/dist/flv.min.js',
    domtoimage: 'https://cdn.jsdelivr.net/npm/dom-to-image@2.6.0/src/dom-to-image.min.js',
    fabric: 'https://cdn.jsdelivr.net/npm/fabric@4.3.1/dist/fabric.min.js',
    };

    const loadScript = (src) => {
    return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.type = 'text/javascript';
    script.onload = resolve;
    script.onerror = reject;
    script.crossOrigin = 'anonymous';
    script.src = src;
    if (document.head.append) {
    document.head.append(script);
    } else {
    document.getElementsByTagName('head')[0].appendChild(script);
    }
    });
    };

    const loadByKey = (key) => {
    // 判断对应变量在 window 是否存在,如果存在说明已加载,直接返回,这样可以避免多次重复加载
    if (window[key]) {
    return Promise.resolve();
    } else {
    if (Array.isArray(jsUrls[key])) {
    return Promise.all(jsUrls[key].map(loadScript));
    }
    return loadScript(jsUrls[key]);
    }
    };

    // 定义这些方法只是为了方便使用,其实 loadByKey 就够了。
    const loadHtml2Canvas = () => {
    return loadByKey('html2canvas');
    };

    const loadXlsx = () => {
    return loadByKey('XLSX');
    };

    const loadFlvjs = () => {
    return loadByKey('flvjs');
    };

    window.AsyncLoader = {
    loadScript,
    loadByKey,
    loadHtml2Canvas,
    loadXlsx,
    loadFlvjs,
    };
    }

    initLoader();

    使用方式


    以 XLSX 为例,使用这种方式之后,我们不需要在顶部 import xlsx from 'xlsx',只有当用户点击 导出Excel 按钮的时候,才从 CDN 动态加载 xlsx.min.js,加载成功后使用 window.XLSX 即可,代码如下:

    await window.AsyncLoader.loadXlsx().then(() => {
    const XLSX = window.XLSX;
    if (resp.data.signList && resp.data.signList.length > 0) {
    const new_workbook = XLSX.utils.book_new();

    resp.data.signList.map((item) => {
    const header = ['班级/学校/单位', '姓名', '帐号', '签到时间'];
    const { signRecords } = item;
    signRecords.unshift(header);

    const worksheet = XLSX.utils.aoa_to_sheet(signRecords);
    XLSX.utils.book_append_sheet(new_workbook, worksheet, item.signName);
    });

    XLSX.writeFile(new_workbook, `${resp.data.fileName}.xlsx`);
    } else {
    const new_workbook = XLSX.utils.book_new();
    const header = [['班级/学校/单位', '姓名', '帐号']];
    const worksheet = XLSX.utils.aoa_to_sheet(header);
    XLSX.utils.book_append_sheet(new_workbook, worksheet, '');
    XLSX.writeFile(new_workbook, `${resp.data.fileName}.xlsx`);
    }
    });

    另一个动态加载 domtoimage 的示例

    window.CommonJsLoader.loadByKey('domtoimage').then(() => {
    const scale = 2;
    window.domtoimage
    .toPng(poster, {
    height: poster.offsetHeight * scale,
    width: poster.offsetWidth * scale,
    style: {
    zoom: 1,
    transform: `scale(${scale})`,
    transformOrigin: 'top left',
    width: `${poster.offsetWidth}px`,
    height: `${poster.offsetHeight}px`,
    },
    })
    .then((dataUrl) => {
    copyImage(dataUrl, liveData?.planName);
    message.success(`${navigator.clipboard ? '复制' : '下载'}成功`);
    });
    });

    AsyncLoader 方案使用方便、理解简单,而且可以很好地利用 CDN 缓存,多个项目可以共用同样的 URL,进一步提高加载速度。而且这种方式使用的是原生 JS,在任何框架中都可以使用。


    注意,如果你用 TypeScript 开发,这种方案或许会丢失一些智能提示,如果引入了对应的 @types/xxx 应该没影响。如果你特别在意开发时的智能提示,也可以在开发的过程中 import 对应的包,开发完成后才换成 AsyncLoader 方案。

    原文:https://juejin.cn/post/6953193301289893901
    收起阅读 »