注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

JS前端面试总结

ES5的继承和ES6的继承有什么区别ES5的继承时通过prototype或构造函数机制来实现。ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.apply(this))。ES6的继承机制完全不同,实质上是先创建父类的实...
继续阅读 »

ES5的继承和ES6的继承有什么区别

ES5的继承时通过prototype或构造函数机制来实现。ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.apply(this))。
ES6的继承机制完全不同,实质上是先创建父类的实例对象this(所以必须先调用父类的super()方法),然后再用子类的构造函数修改this。
具体的:ES6通过class关键字定义类,里面有构造方法,类之间通过extends关键字实现继承。子类必须在constructor方法中调用super方法,否则新建实例报错。因为子类没有自己的this对象,而是继承了父类的this对象,然后对其进行加工。如果不调用super方法,子类得不到this对象。
ps:super关键字指代父类的实例,即父类的this对象。在子类构造函数中,调用super后,才可使用this关键字,否则报错。

如何实现一个闭包?闭包的作用有哪些

在一个函数里面嵌套另一个函数,被嵌套的那个函数的作用域是一个闭包。
作用:创建私有变量,减少全局变量,防止变量名污染。可以操作外部作用域的变量,变量不会被浏览器回收,保存变量的值。

介绍一下 JS 有哪些内置对象

Object 是 JavaScript 中所有对象的父对象
数据封装类对象:Object、Array、Boolean、Number、String
其他对象:Function、Argument、Math、Date、RegExp、Error

new 操作符具体干了什么呢

(1)创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型。
(2)属性和方法被加入到 this 引用的对象中。
(3)新创建的对象由 this 所引用,并且最后隐式的返回 this 。

同步和异步的区别

同步的概念应该是来自于操作系统中关于同步的概念:不同进程为协同完成某项工作而在先后次序上调整(通过阻塞,唤醒等方式)。
同步强调的是顺序性,谁先谁后;异步则不存在这种顺序性。

同步:浏览器访问服务器请求,用户看得到页面刷新,重新发请求,等请求完,页面刷新,新内容出现,用户看到新内容,进行下一步操作。

异步:浏览器访问服务器请求,用户正常操作,浏览器后端进行请求。等请求完,页面不刷新,新内容也会出现,用户看到新内容。

异步解决方式优缺点

回调函数(callback)

缺点:回调地狱,不能用 try catch 捕获错误,不能 return
优点:解决了同步的问题

Promise

Promise就是为了解决callback的问题而产生的。
回调地狱的根本问题在于:

缺乏顺序性: 回调地狱导致的调试困难,和大脑的思维方式不符
嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身,即(控制反转)
嵌套函数过多的多话,很难处理错误

Promise 实现了链式调用,也就是说每次 then 后返回的都是一个全新 Promise,如果我们在 then 中 return ,return 的结果会被 Promise.resolve() 包装

优点:解决了回调地狱的问题
缺点:无法取消 Promise ,错误需要通过回调函数来捕获

Generator

特点:可以控制函数的执行,可以配合 co 函数库使用

Async/await

async、await 是异步的终极解决方案

优点:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题
缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。

null 和 undefined 的区别

null: null表示空值,转为数值时为0;
undefined:undefined表示"缺少值",就是此处应该有一个值,但是还没有定义。

• 变量被声明了,但没有赋值时,就等于undefined。
• 对象没有赋值的属性,该属性的值为undefined。
• 函数没有返回值时,默认返回undefined。

JavaScript 原型,原型链 ? 有什么特点?

JavaScript 原型: 每创建一个函数,函数上都有一个属性为 prototype,它的值是一个对象。 这个对象的作用在于当使用函数创建实例的时候,那么这些实例都会共享原型上的属性和方法。

原型链: 在 JavaScript 中,每个对象都有一个指向它的原型(prototype)对象的内部链接(proto)。这个原型对象又有自己的原型,直到某个对象的原型为 null 为止(也就是不再有原型指向)。这种一级一级的链结构就称为原型链(prototype chain)。 当查找一个对象的属性时,JavaScript 会向上遍历原型链,直到找到给定名称的属性为止;到查找到达原型链的顶部(Object.prototype),仍然没有找到指定的属性,就会返回 undefined

如何获取一个大于等于0且小于等于9的随机整数

function randomNum(){
return Math.floor(Math.random()*10)
}

想要去除一个字符串的第一个字符,有哪些方法可以实现str.slice(1)

 str.substr(1)
str.substring(1)
str.replace(/./,'')
str.replace(str.charAt(0),'')

JavaScript的组成

JavaScript 由以下三部分组成:

ECMAScript(核心):JavaScript 语言基础
DOM(文档对象模型):规定了访问HTML和XML的接口
BOM(浏览器对象模型):提供了浏览器窗口之间进行交互的对象和方法

到底什么是前端工程化、模块化、组件化

前端工程化就是用做工程的思维看待和开发自己的项目,
而模块化和组件化是为工程化思想下相对较具体的开发方式,因此可以简单的认为模块化和组件化是工程化的表现形式。
模块化和组件化一个最直接的好处就是复用,同时我们也应该有一个理念,模块化和组件化除了复用之外还有就是分治,我们能够在不影响其他代码的情况下按需修改某一独立的模块或是组件,因此很多地方我们及时没有很强烈的复用需要也可以根据分治需求进行模块化或组件化开发。
模块化开发的4点好处:

  1 避免变量污染,命名冲突
  2 提高代码复用率
  3 提高维护性
4 依赖关系的管理

前端模块化实现的过程如下:
一 函数封装
我们在讲到函数逻辑的时候提到过,函数一个功能就是实现特定逻辑的一组语句打包,在一个文件里面编写几个相关函数就是最开始的模块了

function m1(){
    //...
  }

  function m2(){
    //...
  }

这样做的缺点很明显,污染了全局变量,并且不能保证和其他模块起冲突,模块成员看起来似乎没啥关系
二 对象
为了解决这个问题,有了新方法,将所有模块成员封装在一个对象中

var module = new Object({

_count:0,

m1:function (){ ``` },

m2:function (){ ``` }

})

这样 两个函数就被包在这个对象中, 嘿嘿 看起来没毛病是吗 继续往下:
当我们要使用的时候,就是调用这个对象的属性
module.m1()
诶嘿 那么问题来了 这样写法会暴露全部的成员,内部状态可以被外部改变,比如外部代码可直接改变计数器的值
//坏人的操作

module._count = 10;

最后的最后,聪明的人类找到了究极新的方法——立即执行函数,这样就可以达到不暴露私有成员的目的

var module = (function (){

var _count = 5;

var m1 = function (){ ``` };

var m2 = function (){ ``` };

return{
m1:m1,
m2:m2
}

})()

面向对象与面向过程

  1. 什么是面向过程与面向对象?

• 面向过程就是做围墙的时候,由你本身操作,叠第一层的时候:放砖头,糊水泥,放砖头,糊水泥;然后第二层的时候,继续放砖头,糊水泥,放砖头,糊水泥……
• 面向对象就是做围墙的时候,由他人帮你完成,将做第一层的做法抽取出来,就是放砖头是第一个动作,糊水泥是第二个动作,然后给这两个动作加上步数,最后告诉机器人有 n 层,交给机器人帮你工作就行了。

  1. 为什么需要面向对象写法?

• 更方便
• 可以复用,减少代码冗余度
• 高内聚低耦合
简单来说,就是增加代码的可复用性,减少咱们的工作,使代码更加流畅。

事件绑定和普通事件有什么区别

普通添加事件的方法:

var btn = document.getElementById("hello");
btn.onclick = function(){
alert(1);
}
btn.onclick = function(){
alert(2);
}

执行上面的代码只会alert 2

事件绑定方式添加事件:

var btn = document.getElementById("hello");
btn.addEventListener("click",function(){
alert(1);
},false);
btn.addEventListener("click",function(){
alert(2);
},false);

垃圾回收

由于字符串、对象和数组没有固定大小,所有当他们的大小已知时,才能对他们进行动态的存储分配。JavaScript程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则,JavaScript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。
  现在各大浏览器通常用采用的垃圾回收有两种方法:标记清除、引用计数。
1、标记清除
  这是javascript中最常用的垃圾回收方式。当变量进入执行环境是,就标记这个变量为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到他们。当变量离开环境时,则将其标记为“离开环境”。
  垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。
关于这一块,建议读读Tom大叔的几篇文章,关于作用域链的一些知识详解,读完差不多就知道了,哪些变量会被做标记。

2、引用计数
  另一种不太常见的垃圾回收策略是引用计数。引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其所占的内存空间给收回来。这样,垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占的内存。


原文链接:https://segmentfault.com/a/1190000018077712


收起阅读 »

面向面试编程,面向掘金面试

我使用 curl 与 jq 一行简单的命令爬取了掘金的面试集合榜单,有兴趣的同学可以看看爬取过程: 使用 jq 与 sed 制作掘金面试文章排行榜,可以提高你使用命令行的乐趣关于前端,后端,移动端的面试,这里统...
继续阅读 »

我使用 curl 与 jq 一行简单的命令爬取了掘金的面试集合榜单,有兴趣的同学可以看看爬取过程: 使用 jq 与 sed 制作掘金面试文章排行榜,可以提高你使用命令行的乐趣

关于前端,后端,移动端的面试,这里统统都有,希望可以在面试的过程中帮助到你。另外我也有一个仓库 日问 来记录前后端以及 devops 一些有意思的问题,欢迎交流

前端

后端

Android/IOS

原文:https://segmentfault.com/a/1190000021037487

收起阅读 »

通用的广告栏控件-ConvenientBanner

demo:ConvenientBanner通用的广告栏控件,让你轻松实现广告头效果。支持无限循环,可以设置自动翻页和时间(而且非常智能,手指触碰则暂停翻页,离开自动开始翻页。你也可以设置在界面onPause的时候不进行自动翻页,onResume之后继续自动翻页...
继续阅读 »

demo:



ConvenientBanner

通用的广告栏控件,让你轻松实现广告头效果。支持无限循环,可以设置自动翻页和时间(而且非常智能,手指触碰则暂停翻页,离开自动开始翻页。你也可以设置在界面onPause的时候不进行自动翻页,onResume之后继续自动翻页),并且提供多种翻页特效。 对比其他广告栏控件,大多都需要对源码进行改动才能加载网络图片,或者帮你集成不是你所需要的图片缓存库。而这个库能让有代码洁癖的你欢喜,不需要对库源码进行修改你就可以使用任何你喜欢的网络图片库进行配合。

demo是用Module方式依赖,你也可以使用gradle 依赖:

    implementation 'com.bigkoo:convenientbanner:2.1.5'//地址变小写了,额。。。
implementation 'androidx.recyclerview:recyclerview:1.0.0+'

// compile 'com.bigkoo:ConvenientBanner:2.1.4'//地址变ConvenientBanner 大写了,额。。。
//compile 'com.bigkoo:convenientbanner:2.0.5'旧版
Config in xml
<com.bigkoo.convenientbanner.ConvenientBanner
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/convenientBanner"
android:layout_width="match_parent"
android:layout_height="200dp"
app:canLoop="true" //控制循环与否
/>

config in java code

//自定义你的Holder,实现更多复杂的界面,不一定是图片翻页,其他任何控件翻页亦可。
convenientBanner.setPages(
new CBViewHolderCreator() {
@Override
public LocalImageHolderView createHolder(View itemView) {
return new LocalImageHolderView(itemView);
}

@Override
public int getLayoutId() {
return R.layout.item_localimage;
}
}, localImages)
//设置两个点图片作为翻页指示器,不设置则没有指示器,可以根据自己需求自行配合自己的指示器,不需要圆点指示器可用不设
// .setPageIndicator(new int[]{R.drawable.ic_page_indicator, R.drawable.ic_page_indicator_focused})
.setOnItemClickListener(this);
//设置指示器的方向
// .setPageIndicatorAlign(ConvenientBanner.PageIndicatorAlign.ALIGN_PARENT_RIGHT)
// .setOnPageChangeListener(this)//监听翻页事件
;

public class LocalImageHolderView implements Holder<Integer>{
private ImageView imageView;
@Override
public View createView(Context context) {
imageView = new ImageView(context);
imageView.setScaleType(ImageView.ScaleType.FIT_XY);
return imageView;
}

@Override
public void UpdateUI(Context context, final int position, Integer data) {
imageView.setImageResource(data);
}
}


原文链接:https://github.com/saiwu-bigkoo/Android-ConvenientBanner

代码下载:Android-ConvenientBanner-master.zip

收起阅读 »

iOS面试题(二)

数据结构:objc_object,objc_class,isa,class_data_bits_t,cache_t,method_t 对象,类对象,元类对象 消息传递 消息转发 一、数据结构:objc_object,objc_class,isa,class...
继续阅读 »
  • 数据结构:objc_object,objc_class,isa,class_data_bits_t,cache_t,method_t

  • 对象,类对象,元类对象

  • 消息传递

  • 消息转发



一、数据结构:objc_object,objc_class,isa,class_data_bits_t,cache_t,method_t







  • objc_object(id)
    isa_t,关于isa操作相关,弱引用相关,关联对象相关,内存管理相关

  • objc_class (class) 继承自objc_object

  • isa指针,共用体isa_t


  • isa指向

    关于对象,其指向类对象。

    关于类对象,其指向元类对象。

    实例--(isa)-->class--(isa)-->MetaClass

  • cache_t

    用于快速查找方法执行函数,是可增量扩展的哈希表结构,是局部性原理的最佳运用


 struct cache_t {
struct bucket_t *_buckets;//一个散列表,用来方法缓存,bucket_t类型,包含key以及方法实现IMP
mask_t _mask;//分配用来缓存bucket的总数
mask_t _occupied;//表明目前实际占用的缓存bucket的个数

struct bucket_t {
private:
cache_key_t _key;
IMP _imp;

复制代码


  • class_data_bits_t:对class_rw_t的封装


struct class_rw_t {
uint32_t flags;
uint32_t version;

const class_ro_t *ro;

method_array_t methods;
property_array_t properties;
protocol_array_t protocols;

Class firstSubclass;
Class nextSiblingClass;

char *demangledName;

复制代码

Objc的类的属性、方法、以及遵循的协议都放在class_rw_t中,class_rw_t代表了类相关的读写信息,是对class_ro_t的封装,而class_ro_t代表了类的只读信息,存储了 编译器决定了的属性、方法和遵守协议


struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif

const uint8_t * ivarLayout;

const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;

const uint8_t * weakIvarLayout;
property_list_t *baseProperties;

method_list_t *baseMethods() const {
return baseMethodList;
}
};
复制代码


  • method_t

    函数四要素:名称,返回值,参数,函数体


struct method_t {
SEL name; //名称
const char *types;//返回值和参数
IMP imp; //函数体

复制代码

二、 对象,类对象,元类对象



  • 类对象存储实例方法列表等信息。

  • 元类对象存储类方法列表等信息。


  • superClass是一层层集成的,到最后NSObject的superClass是nil.而NSObject的isa指向根元类,这个根元类的isa指向它自己,而它的superClass是NSObject,也就是最后形成一个环,

    三、消息传递


    void objc_msgSend(void /* id self, SEL op, ... */ )

    void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )

    struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;

    /// Specifies the particular superclass of the instance to message.
    #if !defined(__cplusplus) && !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
    #else
    __unsafe_unretained _Nonnull Class super_class;
    #endif
    /* super_class is the first class to search */
    };
    复制代码



消息传递的流程:缓存查找-->当前类查找-->父类逐级查找



  • 调用方法之前,先去查找缓存,看看缓存中是否有对应选择器的方法实现,如果有,就去调用函数,完成消息传递(缓存查找:给定值SEL,目标是查找对应bucket_t中的IMP,哈希查找)

  • 如果缓存中没有,会根据当前实例的isa指针查找当前类对象的方法列表,看看是否有同样名称的方法 ,如果找到,就去调用函数,完成消息传递(当前类中查找:对于已排序好的方法列表,采用二分查找,对于没有排序好的列表,采用一般遍历)

  • 如果当前类对象的方法列表没有,就会逐级父类方法列表中查找,如果找到,就去调用函数,完成消息传递(父类逐级查找:先判断父类是否为nil,为nil则结束,否则就继续进行缓存查找-->当前类查找-->父类逐级查找的流程)

  • 如果一直查到根类依然没有查找到,则进入到消息转发流程中,完成消息传递


四、消息转发


+ (BOOL)resolveInstanceMethod:(SEL)sel;//为对象方法进行决议
+ (BOOL)resolveClassMethod:(SEL)sel;//为类方法进行决议
- (id)forwardingTargetForSelector:(SEL)aSelector;//方法转发目标
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
复制代码





那么最后消息未能处理的时候,还会调用到

- (void)doesNotRecognizeSelector:(SEL)aSelector这个方法,我们也可以在这个方法中做处理,避免掉crash,但是只建议在线上环境的时候做处理,实际开发过程中还要把异常抛出来

方法交换(Method-Swizzling)
+ (void)load
{
Method test = class_getInstanceMethod(self, @selector(test));

Method otherTest = class_getInstanceMethod(self, @selector(otherTest));

method_exchangeImplementations(test, otherTest);
}

应用场景:替换系统的方法,比如viewDidLoad,viewWillAppear以及一些响应方法,来进行统计信息

动态添加方法

class_addMethod(self, sel, testImp, "v@:");

void testImp (void)
{
NSLog(@"testImp");
}

  • @dynamic 动态方法解析

    动态运行时语言将函数决议推迟到运行时

    编译时语言在编译期进行函数决议


  • [obj foo]和objc_msgSend()函数之间有什么关系?

    objc_msgSend()是[obj foo]的具体实现。在runtime中,objc_msgSend()是一个c函数,[obj foo]会被翻译成这样的形式objc_msgSend(obj, foo)。


  • runtime是如何通过selector找到对应的IMP地址的?

    缓存查找-->当前类查找-->父类逐级查找


  • 能否向编译后的类中增加实例变量?

    不能。 编译后,该类已经完成了实例变量的布局,不能再增加实例变量。

    但可以向动态添加的类中增加实例变量。


链接:https://juejin.cn/post/6844904039004504072 收起阅读 »

iOS面试题(一)

字符串反转链表反转有序数组合并Hash算法查找两个子视图的共同父视图求无序数组当中的中位数一、字符串反转给定字符串 "hello,world",实现将其反转。输出结果:dlrow,olleh- (void)charReverse { NSString ...
继续阅读 »
  • 字符串反转
  • 链表反转
  • 有序数组合并
  • Hash算法
  • 查找两个子视图的共同父视图
  • 求无序数组当中的中位数

一、字符串反转
给定字符串 "hello,world",实现将其反转。输出结果:dlrow,olleh

- (void)charReverse
{
NSString * string = @"hello,world";

NSLog(@"%@",string);

NSMutableString * reverString = [NSMutableString stringWithString:string];

for (NSInteger i = 0; i < (string.length + 1)/2; i++) {

[reverString replaceCharactersInRange:NSMakeRange(i, 1) withString:[string substringWithRange:NSMakeRange(string.length - i - 1, 1)]];

[reverString replaceCharactersInRange:NSMakeRange(string.length - i - 1, 1) withString:[string substringWithRange:NSMakeRange(i, 1)]];
}

NSLog(@"reverString:%@",reverString);

//C
char ch[100];

memcpy(ch, [string cStringUsingEncoding:NSUTF8StringEncoding], [string length]);

//设置两个指针,一个指向字符串开头,一个指向字符串末尾
char * begin = ch;

char * end = ch + strlen(ch) - 1;

//遍历字符数组,逐步交换两个指针所指向的内容,同时移动指针到对应的下个位置,直至begin>=end
while (begin < end) {

char temp = *begin;

*(begin++) = *end;

*(end--) = temp;
}

NSLog(@"reverseChar[]:%s",ch);
}
复制代码

二、链表反转
反转前:1->2->3->4->NULL
反转后:4->3->2->1->NULL

/**  定义一个链表  */
struct Node {

NSInteger data;

struct Node * next;
};

- (void)listReverse
{
struct Node * p = [self constructList];

[self printList:p];

//反转后的链表头部
struct Node * newH = NULL;
//头插法
while (p != NULL) {

//记录下一个结点
struct Node * temp = p->next;
//当前结点的next指向新链表的头部
p->next = newH;
//更改新链表头部为当前结点
newH = p;
//移动p到下一个结点
p = temp;
}

[self printList:newH];
}
/**
打印链表

@param head 给定链表
*/

- (void)printList:(struct Node *)head
{
struct Node * temp = head;

printf("list is : ");

while (temp != NULL) {

printf("%zd ",temp->data);

temp = temp->next;
}

printf("\n");
}


/** 构造链表 */
- (struct Node *)constructList
{
//头结点
struct Node *head = NULL;
//尾结点
struct Node *cur = NULL;

for (NSInteger i = 0; i < 10; i++) {

struct Node *node = malloc(sizeof(struct Node));

node->data = i;

//头结点为空,新结点即为头结点
if (head == NULL) {

head = node;

}else{
//当前结点的next为尾结点
cur->next = node;
}

//设置当前结点为新结点
cur = node;
}

return head;
}

复制代码

三、有序数组合并
将有序数组 {1,4,6,7,9} 和 {2,3,5,6,8,9,10,11,12} 合并为
{1,2,3,4,5,6,6,7,8,9,9,10,11,12}

- (void)orderListMerge
{
int aLen = 5,bLen = 9;

int a[] = {1,4,6,7,9};

int b[] = {2,3,5,6,8,9,10,11,12};

[self printList:a length:aLen];

[self printList:b length:bLen];

int result[14];

int p = 0,q = 0,i = 0;//p和q分别为a和b的下标,i为合并结果数组的下标

//任一数组没有达到s边界则进行遍历
while (p < aLen && q < bLen) {

//如果a数组对应位置的值小于b数组对应位置的值,则存储a数组的值,并移动a数组的下标与合并结果数组的下标
if (a[p] < b[q]) result[i++] = a[p++];

//否则存储b数组的值,并移动b数组的下标与合并结果数组的下标
else result[i++] = b[q++];
}

//如果a数组有剩余,将a数组剩余部分拼接到合并结果数组的后面
while (++p < aLen) {

result[i++] = a[p];
}

//如果b数组有剩余,将b数组剩余部分拼接到合并结果数组的后面
while (q < bLen) {

result[i++] = b[q++];
}

[self printList:result length:aLen + bLen];
}
- (void)printList:(int [])list length:(int)length
{
for (int i = 0; i < length; i++) {

printf("%d ",list[i]);
}

printf("\n");
}
复制代码

四、HASH算法

  • 哈希表
    例:给定值是字母a,对应ASCII码值是97,数组索引下标为97。
    这里的ASCII码,就算是一种哈希函数,存储和查找都通过该函数,有效地提高查找效率。
  • 在一个字符串中找到第一个只出现一次的字符。如输入"abaccdeff",输出'b'

    字符(char)是一个长度为8的数据类型,因此总共有256种可能。每个字母根据其ASCII码值作为数组下标对应数组种的一个数字。数组中存储的是每个字符出现的次数。
- (void)hashTest
{
NSString * testString = @"hhaabccdeef";

char testCh[100];

memcpy(testCh, [testString cStringUsingEncoding:NSUTF8StringEncoding], [testString length]);

int list[256];

for (int i = 0; i < 256; i++) {

list[i] = 0;
}

char *p = testCh;

char result = '\0';

while (*p != result) {

list[*(p++)]++;
}

p = testCh;

while (*p != result) {

if (list[*p] == 1) {

result = *p;

break;
}

p++;
}

printf("result:%c",result);
}
复制代码

五、查找两个子视图的共同父视图
思路:分别记录两个子视图的所有父视图并保存到数组中,然后倒序寻找,直至找到第一个不一样的父视图。

- (void)findCommonSuperViews:(UIView *)view1 view2:(UIView *)view2
{
NSArray * superViews1 = [self findSuperViews:view1];

NSArray * superViews2 = [self findSuperViews:view2];

NSMutableArray * resultArray = [NSMutableArray array];

int i = 0;

while (i < MIN(superViews1.count, superViews2.count)) {

UIView *super1 = superViews1[superViews1.count - i - 1];

UIView *super2 = superViews2[superViews2.count - i - 1];

if (super1 == super2) {

[resultArray addObject:super1];

i++;

}else{

break;
}
}

NSLog(@"resultArray:%@",resultArray);

}
- (NSArray *)findSuperViews:(UIView *)view
{
UIView * temp = view.superview;

NSMutableArray * result = [NSMutableArray array];

while (temp) {

[result addObject:temp];

temp = temp.superview;
}

return result;
}
复制代码

六、求无序数组中的中位数
中位数:当数组个数n为奇数时,为(n + 1)/2,即是最中间那个数字;当n为偶数时,为(n/2 + (n/2 + 1))/2,即是中间两个数字的平均数。
首先要先去了解一些几种排序算法:iOS排序算法
思路:

  • 1.排序算法+中位数
    首先用冒泡排序、快速排序、堆排序、希尔排序等排序算法将所给数组排序,然后取出其中位数即可。
  • 2.利用快排思想
链接:https://juejin.cn/post/6844904038996279309
收起阅读 »

vue 自动化路由实现

1、需求描述在写vue的项目中,一般情况下我们每添加一个新页面都得添加一个新路由。为此我们在项目中会专门的一个文件夹来管理路由,如下图所示那么有没有一种方案,能够实现我们在文件夹中新建了一个vue文件,就自动帮我们添加路由。特别在我们的一个ERP后台项目中,我...
继续阅读 »

1、需求描述

在写vue的项目中,一般情况下我们每添加一个新页面都得添加一个新路由。为此我们在项目中会专门的一个文件夹来管理路由,如下图所示


那么有没有一种方案,能够实现我们在文件夹中新建了一个vue文件,就自动帮我们添加路由。特别在我们的一个ERP后台项目中,我们几乎都是一个文件夹下有很多子文件,子文件中一般包含index.vue, detail.vue, edit.vue分别对应的事列表页,详情页和编辑页。


 上图是我们的文件目录,views文件夹中存放的是所有的页面,goodsPlanning是一级目录,onNewComplete和thirdGoods是二级目录,二级目录中存放的是具体的页面,indexComponents中存放的是index.vue的文件,editComponents也是同样的道理。index.vue对应的路由是/goodsPlanning/onNewComplete, edit.vue对应的路由是/goodsPlanning/onNewComplete/edit,detail.vue也是同样的道理。所以我们的文件夹和路由是完全能够对应上的,只要知道路由,就能很快的找到对应的文件。那么有没有办法能够读取我们二级目录下的所有文件,然后根据文件名来生成路由呢?答案是有的


2 、require.context介绍

简单说就是:有了require.context,我们可以得到指定文件夹下的所有文件

require.context(directory, useSubdirectories = false, regExp = /^\.\//);

require.context有三个参数:

  • directory:说明需要检索的目录
  • useSubdirectories:是否检索子目录
  • regExp: 匹配文件的正则表达式

require.context()的返回值,有一个keys方法,返回的是个数组

let routers = require.context('VIEWS', true).keys()
console.log(routers)



 通过上面的代码,我们打印出了所有的views文件夹下的所有文件和文件夹,我们只要写好正则就能找到我们所需要的文件


3、 直接上代码

import Layout from 'VIEWS/layout/index'

/**
* 正则 首先匹配./ ,然后一级目录,不包含components的二级目录,以.vue结尾的三级目录
*/
let routers = require.context('VIEWS', true, /\.\/[a-z]+\/(?!components)[a-z]+\/[a-z]+\.vue$/i).keys()
let indexRouterMap = {} // 用来存储以index.vue结尾的文件,因为index.vue是列表文件,需要加入layout(我们的菜单),需要keepAlive,需要做权限判断
let detailRouterArr = [] // 用来存储以非index.vue结尾的vue文件,此类目前不需要layout
routers.forEach(item => {
const paths = item.match(/[a-zA-Z]+/g) //paths中存储了一个目录,二级目录,文件名
const routerChild = { //定义路由对象
path: paths[1],
name: `${paths[0]}${_.upperFirst(paths[1])}`, //upperFirst,lodash 首字母大写方法
component(resolve) {
require([`../../views${item.slice(1)}`], resolve)
},
}
if (/index\.vue$/.test(item)) { //判断是否以indexvue结尾
if (indexRouterMap[paths[0]]) { //判断一级路由是否存在,存在push二级路由,不存在则新建
indexRouterMap[paths[0]].children.push(routerChild)
} else {
indexRouterMap[paths[0]] = {
path: '/' + paths[0],
component: Layout,
children: [routerChild]
}
}
} else { //不以index.vue结尾的,直接添加到路由中
detailRouterArr.push({
path: item.slice(1, -4), //渠道最前面的 . 和最后的.vue
name: `${paths[0]}${_.upperFirst(paths[1])}${_.upperFirst(paths[2])}`,
component(resolve) {
require([`../../views${item.slice(1)}`], resolve)
},
meta: {
noCache: true, //不keepAlive
noVerify: true //不做权限验证
}
})
}
})

export default [
...Object.values(indexRouterMap),
...detailRouterArr,
/**
* dashboard单独处理下
*/
{
path: '',
component: Layout,
redirect: 'dashboard',
children: [
{
path: 'dashboard',
component: () => import('VIEWS/dashboard/index'),
name: 'dashboard',
meta: { title: '首页', noCache: true, noVerify: true }
}
]
},
]

简简单单的几十行代码就实现了所有的路由功能,再也不用一行一行的写路由文件了。可能你的文件管理方式和我的不一样,但是只要稍微改改正则就行了。


4、 注意

  1. 不能用import引入路由因为用import引入不支持变量
  2. 不能用别名找了半天问题,才知道用变量时也不能用别名,所以我用的都是相对路径


5、 好处

  • 不用在添加路由了,这个就不说了,明眼人都看得出来
  • 知道了路由,一个能找到对应的文件,以前我们团队就出现过,乱写path的情况
  • 更好的控制验证和keepAlive

原文链接:https://www.cnblogs.com/mianbaodaxia/p/11452123.html

收起阅读 »

iOS基础之Category(一)

一、简介 我们可以利用 category 把类的实现分开在几个不同的文件中,这样可以减少单个文件的体积。可以把不同的功能组织到不同的 category 里使功能单一化。可以由多个开发者共同完成一个类,只需各自创建该类的 category 即可。可以按需加载想要...
继续阅读 »

一、简介


  1. 我们可以利用 category 把类的实现分开在几个不同的文件中,这样可以减少单个文件的体积。可以把不同的功能组织到不同的 category 里使功能单一化。可以由多个开发者共同完成一个类,只需各自创建该类的 category 即可。可以按需加载想要的 category,比如 SDWebImage 中 UIImageView+WebCache 和 UIButton+WebCache,根据不同需求加载不同的 category。


二、Extension 和 Category 对比



  • extension 是在编译器决定的,它就是类的一部分,在编译期和头文件里的 @interface 和 实现文件里的 @implementation形成一个完整的类,它伴随类的的产生而产生,随着类的消亡而消亡。extension 一般用来隐藏类的私有信息,必须有类的源码才可以为一个类添加 extension。所以无法为系统的类添加 extension

  • category 是在运行期决定的,category 是无法添加实例变量的,extension 是可以添加的。


三、Category 的本质

3.1 Category的基本使用



我们首先来看以下 category的基本使用:


// Person+Eat.h

#import "Person.h"

@interface Person (Eat)

- (void)eatBread;

+ (void)eatFruit;

@property (nonatomic, assign) int count;

@end

// Person+Eat.m

#import "Person+Eat.h"

@implementation Person (Eat)

- (void)eatBread {
NSLog(@"eatBread");
}

+ (void)eatFruit {
NSLog(@"eatFruit");
}

@end
复制代码


  • 创建了一个 Person 的分类,专门实现吃这个功能

  • 这个分类遵守了2个协议,分别为 NSCopyingNSCoding

  • 声明了2个方法,一个实例方法,一个类方法

  • 定义一个 count 属性


3.2 编译期的 Category


我们通过 clang 编译器来观察一下在编译期这些代码的本质是什么?


xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc MyClass.m -o MyClass-arm64.cpp
复制代码

编译之后,我们可以发现 category 的本质是结构体 category_t,无论我们创建了多少个 category 最终都会生成 category_t 这个结构体,并且 category 中的方法、属性、协议都是存储在这个结构体里的。也就是说在编译期,分类中成员是不会和类合并在一起的


struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
};
复制代码


  • name:类的名字

  • cls:类

  • instanceMethodscategory 中所有给类添加的实例方法的列表

  • classMethodscategory 中所有给类添加的类方法的列表

  • protocolscategory 中实现的所有协议的列表

  • instancePropertiescategory 中添加的所有属性


category 的定义中可以看到我们可以 添加实例方法,添加类方法,可以实现协议,可以添加属性。


不可以添加实例变量


我们继续研究下面的编译后的代码:


static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"eatBread", "v16@0:8", (void *)_I_Person_Eat_eatBread}}
};

static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"eatFruit", "v16@0:8", (void *)_C_Person_Eat_eatFruit}}
};

static struct /*_protocol_list_t*/ {
long protocol_count; // Note, this is 32/64 bit
struct _protocol_t *super_protocols[2];
} _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
2,
&_OBJC_PROTOCOL_NSCopying,
&_OBJC_PROTOCOL_NSCoding
};

static struct /*_prop_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count_of_properties;
struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
1,
{{"count","Ti,N"}}
};

static struct _category_t _OBJC_$_CATEGORY_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) =
{

"Person",
0, // &OBJC_CLASS_$_Person,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat,
(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_Eat,
};
复制代码


  • 首先看一下 _OBJC_$_CATEGORY_Person_$_Eat 结构体变量中的值,就是分别对应 category_t 的成员,第1个成员就是类名,因为我们声明了实例方法,类方法,遵守了协议,定义了属性,所以我们的结构体变量中这些都会有值。

  • _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat 结构体表示实例方法列表,里面包含了 eatBread 实例方法

  • _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat 结构体包含了 eatFruit 类方法

  • _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat 结构体包含了 NSCopingNSCoding 协议

  • _OBJC_$_PROP_LIST_Person_$_Eat 结构体包含了 count 属性


3.3 运行期的 Category


在研究完编译时期的 category 后,我们进而研究运行时期的 category


objc-runtime-new.mm 的源码中,我们可以最终找到如何将 category 中的方法列表,属性列表,协议列表添加到类中。


static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
int flags)

{
if (slowpath(PrintReplacedMethods)) {
printReplacements(cls, cats_list, cats_count);
}
if (slowpath(PrintConnecting)) {
_objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
}

/*
* Only a few classes have more than 64 categories during launch.
* This uses a little stack, and avoids malloc.
*
* Categories must be added in the proper order, which is back
* to front. To do that with the chunking, we iterate cats_list
* from front to back, build up the local buffers backwards,
* and call attachLists on the chunks. attachLists prepends the
* lists, so the final result is in the expected order.
*/

constexpr uint32_t ATTACH_BUFSIZ = 64;
method_list_t *mlists[ATTACH_BUFSIZ];
property_list_t *proplists[ATTACH_BUFSIZ];
protocol_list_t *protolists[ATTACH_BUFSIZ];

uint32_t mcount = 0;
uint32_t propcount = 0;
uint32_t protocount = 0;
bool fromBundle = NO;
bool isMeta = (flags & ATTACH_METACLASS);
auto rwe = cls->data()->extAllocIfNeeded();

for (uint32_t i = 0; i < cats_count; i++) {
auto& entry = cats_list[i];

method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if ( ) {
if (mcount == ATTACH_BUFSIZ) {
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rwe->methods.attachLists(mlists, mcount);
mcount = 0;
}
mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
fromBundle |= entry.hi->isBundle();
}

property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
if (propcount == ATTACH_BUFSIZ) {
rwe->properties.attachLists(proplists, propcount);
propcount = 0;
}
proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
}

protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
if (protolist) {
if (protocount == ATTACH_BUFSIZ) {
rwe->protocols.attachLists(protolists, protocount);
protocount = 0;
}
protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
}
}

if (mcount > 0) {
prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount, NO, fromBundle);
rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
if (flags & ATTACH_EXISTING) flushCaches(cls);
}

rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);

rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
复制代码


  • rwe->methods.attachLists(mlists, mcount);

  • rwe->protocols.attachLists(protolists, protocount);

  • rwe->properties.attachLists(proplists, propcount);


以上三个函数就是把 category 中的方法、属性和协议列表添加到类中的函数。


继续查看 attchLists 函数的实现:


void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;

if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
}
else {
// 1 list -> many lists
List* oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList;
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}
复制代码


  • 在这段源码中,主要关注2个函数 memmovememcpy

  • memmove 函数的作用是移动内存,将之前的内存向后移动,将原来的方法列表往后移

  • memcpy 函数的作用是内存的拷贝,将 category 中的方法列表复制到上一步移出来的位置。


从上述源码中,可以发现 category 的方法并没有替换原来类已有的方法,如果 category 和原来类中都有某个同名方法,只不过 category 中的方法被放到了新方法列表的前面,在运行时查找方法的时候是按照顺序查找的,一旦找到该方法,就不会向下继续查找了,产生了 category 会覆盖原类方法的假象。



所以我们在 category 定义方法的时候都要加上前缀,以避免意外的重名把类本身的方法”覆盖“掉。




  • 如果多个 category 中存在同名的方法,运行时最终调用哪个方法是由编译器决定的,最后一个参与编译的方法将会先被调用

链接:https://juejin.cn/post/6950833332422705165

收起阅读 »

iOS 常见面试题总结及答案(2)

一.App启动过慢,你可能想到的因素有哪些?1.解析Info.plist   加载相关信息,例如如闪屏 沙箱建立、权限检查2.Mach-O加载 如果是胖二进制文件,寻找合适当前CPU类别的部分加载所有依赖的Mach-O...
继续阅读 »

一.App启动过慢,你可能想到的因素有哪些?

1.解析Info.plist  

 加载相关信息,例如如闪屏 沙箱建立、权限检查

2.Mach-O加载 

如果是胖二进制文件,寻找合适当前CPU类别的部分
加载所有依赖的Mach-O文件(递归调用Mach-O加载的方法)
定位内部、外部指针引用,例如字符串、函数等
执行声明为attribute((constructor))的C函数
加载类扩展(Category)中的方法

3.程序执行

调用main()
调用UIApplicationMain()
调用applicationWillFinishLaunching

影响启动性能的因素

main()函数之前耗时的影响因素
动态库加载越多,启动越慢。
ObjC类越多,启动越慢
C的constructor函数越多,启动越慢
C++静态对象越多,启动越慢
ObjC的+load越多,启动越慢
main()函数之后耗时的影响因素
执行main()函数的耗时
执行applicationWillFinishLaunching的耗时
rootViewController及其childViewController的加载、view及其subviews的加载

优化

纯代码方式而不是storyboard加载首页UI。
对didFinishLaunching里的函数考虑能否挖掘可以延迟加载或者懒加载,需要与各个业务方pm和rd共同check 对于一些已经下线的业务,删减冗余代码。
对于一些与UI展示无关的业务,如微博认证过期检查、图片最大缓存空间设置等做延迟加载。
对实现了+load()方法的类进行分析,尽量将load里的代码延后调用。
上面统计数据显示展示feed的导航控制器页面(NewsListViewController)比较耗时,对于viewDidLoad以及viewWillAppear方法中尽量去尝试少做,晚做,不做。


二.单例的利弊

优点:
1:一个类只被实例化一次,提供了对唯一实例的受控访问。
2:节省系统资源
3:允许可变数目的实例。

缺点:
1:一个类只有一个对象,可能造成责任过重,在一定程度上违背了“单一职责原则”。
2:由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
3:滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。


三.TCP和UDP的区别于联系

TCP为传输控制层协议,为面向连接、可靠的、点到点的通信;
UDP为用户数据报协议,非连接的不可靠的点到多点的通信;
TCP侧重可靠传输,UDP侧重快速传输


四.TCP连接的三次握手

第一次握手:客户端发送 syn 包(syn=j)到服务器,并进入 SYN_SEND 状态,等待服务器确认;

第二次握手:服务器收到 syn 包,必须确认客户的 SYN(ack=j+1),同时自己也发送一个 SYN 包(syn=k),即 SYN+ACK 包,此时服务器进入 SYN_RECV 状态;

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入 ESTABLISHED 状态,完成三次握手。

握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP 连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。断开连接时服务器和客户端均可以主动发起断开 TCP 连接的请求,断开过程需要经过“四次握手”(过程就不细写了,就是服务器和客户端交互,最终确定断开)



五.假如Controller太臃肿,如何优化?

1.将网络请求抽象到单独的类中

方便在基类中处理公共逻辑;
方便在基类中处理缓存逻辑,以及其它一些公共逻辑;
方便做对象的持久化。

2.将界面的封装抽象到专门的类中
构造专门的 UIView 的子类,来负责这些控件的拼装。这是最彻底和优雅的方式,不过稍微麻烦一些的是,你需要把这些控件的事件回调先接管,再都一一暴露回 Controller。

3.构造 ViewModel
借鉴MVVM。具体做法就是将 ViewController 给 View 传递数据这个过程,抽象成构造 ViewModel 的过程

4.专门构造存储类
专门来处理本地数据的存取。

5.整合常量


六.对程序性能的优化你有什么建议?

1.使用复用机制

2.尽可能设置 View 为不透明

3.避免臃肿的 XIB 文件

4.不要阻塞主线程

5.图片尺寸匹配 UIImageView

6.选择合适的容器

7.启用 GZIP 数据压缩

8.View 的复用和懒加载机制

9、缓存

服务器的响应信息(response)。图片。计算值。比如:UITableView 的 row heights。

10.关于图形绘制

11.处理 Memory Warnings

在 AppDelegate 中实现 - [AppDelegate applicationDidReceiveMemoryWarning:] 代理方法。
在 UIViewController 中重载 didReceiveMemoryWarning 方法。
监听 UIApplicationDidReceiveMemoryWarningNotification 通知。

12.复用高开销的对象

13.减少离屏渲染(设置圆角和阴影的时候可以选用绘制的方法)

14.优化 UITableView

通过正确的设置 reuseIdentifier 来重用 Cell。
尽量减少不必要的透明 View。
尽量避免渐变效果、图片拉伸和离屏渲染。
当不同的行的高度不一样时,尽量缓存它们的高度值。
如果 Cell 展示的内容来自网络,确保用异步加载的方式来获取数据,并且缓存服务器的 response。
使用 shadowPath 来设置阴影效果。
尽量减少 subview 的数量,对于 subview 较多并且样式多变的 Cell,可以考虑用异步绘制或重写 drawRect。
尽量优化 - [UITableView tableView:cellForRowAtIndexPath:] 方法中的处理逻辑,如果确实要做一些处理,可以考虑做一次,缓存结果。
选择合适的数据结构来承载数据,不同的数据结构对不同操作的开销是存在差异的。
对于 rowHeight、sectionFooterHeight、sectionHeaderHeight 尽量使用常量。

15.选择合适的数据存储方式

在 iOS 中可以用来进行数据持有化的方案包括:
NSUserDefaults。只适合用来存小数据。
XML、JSON、Plist 等文件。JSON 和 XML 文件的差异在「选择正确的数据格式」已经说过了。
使用 NSCoding 来存档。NSCoding 同样是对文件进行读写,所以它也会面临必须加载整个文件才能继续的问题。
使用 SQLite 数据库。可以配合 FMDB 使用。数据的相对文件来说还是好处很多的,比如可以按需取数据、不用暴力查找等等。
使用 CoreData。也是数据库技术,跟 SQLite 的性能差异比较小。但是 CoreData 是一个对象图谱模型,显得更面向对象;SQLite 就是常规的 DBMS。

16.减少应用启动时间

快速启动应用对于用户来说可以留下很好的印象。尤其是第一次使用时。
保证应用快速启动的指导原则:
尽量将启动过程中的处理分拆成各个异步处理流,比如:网络请求、数据库访问、数据解析等等。
避免臃肿的 XIB 文件,因为它们会在你的主线程中进行加载。重申:Storyboard 没这个问题,放心使用。
注意:在测试程序启动性能的时候,最好用与 Xcode 断开连接的设备进行测试。因为 watchdog 在使用 Xcode 进行调试的时候是不会启动的。

17.使用 Autorelease Pool (内存释放池)

18.imageNamed 和 imageWithContentsOfFile


七.使用drawRect有什么影响?

drawRect方法依赖Core Graphics框架来进行自定义的绘制

缺点:它处理touch事件时每次按钮被点击后,都会用setNeedsDisplay进行强制重绘;而且不止一次,每次单点事件触发两次执行。这样的话从性能的角度来说,对CPU和内存来说都是欠佳的。特别是如果在我们的界面上有多个这样的UIButton实例,那就会很糟糕了

这个方法的调用机制也是非常特别. 当你调用 setNeedsDisplay 方法时, UIKit 将会把当前图层标记为dirty,但还是会显示原来的内容,直到下一次的视图渲染周期,才会将标记为 dirty 的图层重新建立Core Graphics上下文,然后将内存中的数据恢复出来, 再使用 CGContextRef 进行绘制


八.基于CTMediator的组件化方案,有哪些核心组成?

假如主APP调用某业务A,那么需要以下组成部分:

CTMediator类,该类提供了函数 - (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget;
这个函数可以根据targetName生成对象,根据actionName构造selector,然后可以利用performSelector:withObject:方法,在目标上执行动作。

业务A的实现代码,另外要加一个专门的类,用于执行Target Action
类的名字的格式:Target_%@,这里就是Target_A。
这个类里面的方法,名字都以Action_开头,需要传参数时,都统一以NSDictionary*的形式传入。
CTMediator类会创建Target类的对象,并在对象上执行方法。

业务A的CTMediator扩展
扩展里声明了所有A业务的对外接口,参数明确,这样外部调用者可以很容易理解如何调用接口。
在扩展的实现里,对Target, Action需要通过硬编码进行指定。由于扩展的负责方和业务的负责方是相同的,所以这个不是问题。


九.为什么CTMediator方案优于基于Router的方案?

Router的缺点:

在组件化的实施过程中,注册URL并不是充分必要条件。组件是不需要向组件管理器注册URL的,注册了URL之后,会造成不必要的内存常驻。注册URL的目的其实是一个服务发现的过程,在iOS领域中,服务发现的方式是不需要通过主动注册的,使用runtime就可以了。另外,注册部分的代码的维护是一个相对麻烦的事情,每一次支持新调用时,都要去维护一次注册列表。如果有调用被弃用了,是经常会忘记删项目的。runtime由于不存在注册过程,那就也不会产生维护的操作,维护成本就降低了。 由于通过runtime做到了服务的自动发现,拓展调用接口的任务就仅在于各自的模块,任何一次新接口添加,新业务添加,都不必去主工程做操作,十分透明。

在iOS领域里,一定是组件化的中间件为openURL提供服务,而不是openURL方式为组件化提供服务。如果在给App实施组件化方案的过程中是基于openURL的方案的话,有一个致命缺陷:非常规对象(不能被字符串化到URL中的对象,例如UIImage)无法参与本地组件间调度。

在本地调用中使用URL的方式其实是不必要的,如果业务工程师在本地间调度时需要给出URL,那么就不可避免要提供params,在调用时要提供哪些params是业务工程师很容易懵逼的地方。

为了支持传递非常规参数,蘑菇街的方案采用了protocol,这个会侵入业务。由于业务中的某个对象需要被调用,因此必须要符合某个可被调用的protocol,然而这个protocol又不存在于当前业务领域,于是当前业务就不得不依赖public Protocol。这对于将来的业务迁移是有非常大的影响的

CTMediator的优点:

调用时,区分了本地应用调用和远程应用调用。本地应用调用为远程应用调用提供服务。

组件仅通过Action暴露可调用接口,模块与模块之间的接口被固化在了Target-Action这一层,避免了实施组件化的改造过程中,对Business的侵入,同时也提高了组件化接口的可维护性。

方便传递各种类型的参数。


十.内存的使用和优化的注意事项

重用问题:如UITableViewCells、UICollectionViewCells、UITableViewHeaderFooterViews设置正确的reuseIdentifier,充分重用;

尽量把views设置为不透明:当opque为NO的时候,图层的半透明取决于图片和其本身合成的图层为结果,可提高性能;

不要使用太复杂的XIB/Storyboard:载入时就会将XIB/storyboard需要的所有资源,包括图片全部载入内存,即使未来很久才会使用。那些相比纯代码写的延迟加载,性能及内存就差了很多;

选择正确的数据结构:学会选择对业务场景最合适的数组结构是写出高效代码的基础。比如,数组: 有序的一组值。使用索引来查询很快,使用值查询很慢,插入/删除很慢。字典: 存储键值对,用键来查找比较快。集合: 无序的一组值,用值来查找很快,插入/删除很快。
gzip/zip压缩:当从服务端下载相关附件时,可以通过gzip/zip压缩后再下载,使得内存更小,下载速度也更快。

延迟加载:对于不应该使用的数据,使用延迟加载方式。对于不需要马上显示的视图,使用延迟加载方式。比如,网络请求失败时显示的提示界面,可能一直都不会使用到,因此应该使用延迟加载。

数据缓存:对于cell的行高要缓存起来,使得reload数据时,效率也极高。而对于那些网络数据,不需要每次都请求的,应该缓存起来,可以写入数据库,也可以通过plist文件存储。

处理内存警告:一般在基类统一处理内存警告,将相关不用资源立即释放掉
重用大开销对象:一些objects的初始化很慢,比如NSDateFormatter和NSCalendar,但又不可避免地需要使用它们。通常是作为属性存储起来,防止反复创建。

避免反复处理数据:许多应用需要从服务器加载功能所需的常为JSON或者XML格式的数据。在服务器端和客户端使用相同的数据结构很重要;

使用Autorelease Pool:在某些循环创建临时变量处理数据时,自动释放池以保证能及时释放内存;

正确选择图片加载方式:UIImage加载方式


摘自作者:iOS猿_员
原贴链接:https://www.jianshu.com/p/4b4bd4e3feff

收起阅读 »

前端自测清单(前端八股文)

缘起这篇文章主要列举一些自己想到的面试题目,让大家更加熟悉前端八股文。先从性能优化开始吧。性能优化大体可以分为两个,运行时优化加载时优化加载时优化网络优化dns寻址过程tcp的三次握手和四次挥手,以及为何要三次和为何要四次https的握手过程,以及对称加密和非...
继续阅读 »

/zi-ce-qing-dan/featured-image.jpg

缘起

这篇文章主要列举一些自己想到的面试题目,让大家更加熟悉前端八股文。

先从性能优化开始吧。性能优化大体可以分为两个,

  • 运行时优化
  • 加载时优化

加载时优化

网络优化

  • dns寻址过程
  • tcp的三次握手和四次挥手,以及为何要三次和为何要四次
  • https的握手过程,以及对称加密和非对称加密的区别,什么是中间人劫持,ca证书包括哪些内容
  • http1.0,http1.1以及http2.0的区别,多路复用具体指的是什么,keep-alive具体如何体现
  • cdn的原理,cdn什么情况下会回源,cdn的适用场景
  • 浏览器缓存有哪几种,它们的区别是什么,什么时候发生缓存,如何决定缓存哪些文件
  • 了解过websocket么,解释一下websocket的作用

渲染优化

  • 关键渲染路径优化,什么是关键渲染路径,分别如何优化
  • 优化体积,webpack的分包策略,如何配置优化,如何提高构建速度,tree-shaking是什么
  • cssom 的优化,以及html解析过程中,遇到哪些tag会阻塞渲染
  • 雅虎军规说,css尽量放到head里,js放到下方,那么移动端适配的flexiblejs为何要放到css上方呢
  • 影响回流重绘的因素有哪些,如何避免回流,以及bfc是什么,bfc有什么特性,清除浮动的原理是什么

场景:如何优化首屏

除了上以及下面说到的,这里也是分两个层面,

  • 加载时优化
  • 运行时优化

加载

  • 首屏请求和非首屏请求拆分
  • 图片都应该使用懒加载的形式加载
  • 使用preload预加载技术,以及prefetch的dns预解析
  • 与首屏无关的代码可以加async甚至是defer等待网页加载完成后运行

运行

这里跟加载的异常耦合,另作分析吧。

运行时优化

  • 虚拟长列表渲染
  • 图片懒加载
  • 使用事件委托
  • react memo以及pureComponent
  • 使用SSR
  • 。。。

以及一些比较骚的操作,只能特定场景使用,

  • serviceWorker劫持页面
  • 利用worker

更新一波,性能优化之外的面试题,

底层

  • V8是如何实现GC的
  • JS的let,const,call stack,function context,global context。。。的区别
  • this的指向,箭头函数中this和function里的this有什么区别
  • 原型链是什么,继承呢,有几种继承方式,如何实现es6的class
  • eventloop是什么,浏览器的eventloop和nodejs的eventloop有什么区别,nexttick是什么
  • commonjs和AMD,CMD的区别,以及跟ES MODULE的区别
  • 说说require.cache
  • 了解过,洋葱模型没有,它是如何实现的
  • 说说nodejs中的流,也就是stream
  • 你用过ts,说说你常用的ts中的数据类型
  • js的数据类型,weakMap,weakSet和Map以及Set的区别是什么
  • 为何0.1+0.2 不等于0.3,如何解决这个问题
  • js的类型转换
  • 正则表达式
  • 对象循环引用会发生什么问题
  • 如何捕获异步的异常,你能说出几种方案

CSS相关

  • position有哪几种属性,它们的区别是什么
  • 如何实现垂直居中,移动端的呢
  • margin设置百分比,是依据谁的百分比,padding呢
  • 怪异盒模型和一般盒模型有什么区别
  • flex:1代表什么,flex-shrink和flex-grow有什么区别
  • background-size origin基准点在哪里
  • 移动端1px解决方案,以及为何会产生这个问题
  • 移动端高清屏图片的解决方案
  • 说说GPU加速

跨端

  • RN 实现原理
  • 小程序实现原理
  • webview跟h5有什么区别
  • RPC 是什么
  • JSBridge 原理是什么
  • 网页唤起app的原理是什么

服务端

  • oauth2了解过没有,sso呢
  • JWT 如何实现的

网络

除了之前提到的网络问题,当然还有很多,比如

  • 为何使用1x1的gif进行请求埋点
  • TCP 如何进行拥塞控制

安全

  • csrf是什么,防范措施是什么
  • xss如何防范

浏览器相关

  • 跨域是如何产生的,如何解决
  • 如何检查性能瓶颈
  • 打开页面白屏,如何定位问题,或者打开页面CPU100%,如何定位问题
  • jsonp是什么,为何能解决跨域,使用它可能会产生什么问题
  • base64会产生什么问题
  • event.target和event.currTarget有什么区别

框架相关

  • react和vue的区别
  • react的调度原理
  • setstate为何异步
  • key的作用是什么,为何说要使用唯一key,react的diff算法是如何实现的,vue的呢
  • react的事件系统是如何实现的
  • react hook是如何实现的
  • react的通信方式,hoc的使用场景
  • 听过闭包陷阱么,为何会出现这种现象,如何避免
  • vue的响应式原理
  • 为何vue3.x用的是proxy而不是object.defineProperties
  • vue是如何实现对数据的监听的,对数组呢
  • vue中的nexttick是如何实现的
  • fiber是什么,简单说说时间切片如何实现,为何vue不需要时间切片
  • webpack是如何实现的,HMR是如何实现的,可以写个简单的webpack么,webpack的执行流程是怎样的
  • koa源码实现,洋葱模型原理,get/post等这些方法如何set入koa里的,ctx.body为何能直接改变response的body
  • 你简历上写的了解过webpack源码,到哪种程度了(实话说没写koa简单。。

算法相关

  • js大整数加法
  • 双指针
  • 经典排序
  • 动态规划
  • 贪心算法
  • 回溯法
  • DFS
  • BFS
  • 链表操作
  • 线性求值
  • 预处理,前缀和

项目相关

  • 项目中遇到的最大问题是什么,如何解决的
  • nodejs作为中间层的作用是什么

场景题(机试)

  • 如何实现直播上的弹幕组件,要求不能重叠,仿照b站上的弹幕
  • 如何实现动态表单,仿照antd上的form组件
  • 实现一个promise(一般不会这样问)
  • 实现一个限制请求数量的方法
  • 如何实现一个大文件的上传
  • 实现一个eventEmitter
  • 实现一个new,call,bind,apply
  • 实现一个throttle,debound
  • 实现promise.then,finally,all
  • 实现继承,寄生组合继承,instanceof
  • 实现Generator,Aynsc

20.11.20 更新来了

  • React 生命周期,(分三个阶段进行回答,挂载阶段,更新阶段以及卸载阶段)
  • Vue 生命周期 以及其父子组件的生命周期调度顺序
  • 如果让你用强缓存或者协商缓存来缓存资源的话,你会如何使用
  • 作用域是什么,作用域链呢?(这题我想了下,不会利用语言去表达这个东西。。)

目前暂时想到这些,后来有想到的会补充上去。

原文链接:https://steinw.cc/zi-ce-qing-dan/


收起阅读 »

iOS 常见面试题总结及答案(1)

一.    Runloop和线程的关系?1.一一对应的关系,主线程的runloop已经创建,默认开启,子线程的runloop需要手动创建2.runloop在第一次获取时创建,在线程结束时销毁.1.NSTimer在子线程开启一个定时器;控制定...
继续阅读 »

一.    Runloop和线程的关系?

1.一一对应的关系,主线程的runloop已经创建,默认开启,子线程的runloop需要手动创建

2.runloop在第一次获取时创建,在线程结束时销毁.

runloop 的运行逻辑就是 do-while 循环下运用观察者模式(或者说是消息发送),根据7种状态的变化,处理事件输入源和定时器。如下图


runloop的应用:

1.NSTimer在子线程开启一个定时器;控制定时器在特定模式下执行

2.imageView的显示

3.performSelector

4.常驻线程(让一个子线程不进入消亡状态,等待其他线程发来消息,处理其他事件)

5.自动释放池

二.自动释放池什么时候释放?

第一次创建:启动runloop时候

最后一次销毁:runloop退出的时候

其他时候的创建和销毁:当runloop即将睡眠时销毁之前的释放池,重建一个新的

三.什么时候使用weak关键字,和assign的区别?

1.arc中有可能出现循环引用的地方,比如delegate属性

2.自定义IBOutlet空间属性一般也是使用weak

区别:weak表明一种非持有关系,必须用于oc对象;assign用于基本数据类型

weak修饰的指针默认是nil,如果用assign修饰对象,在对象被销毁时,会产生野指针,容易发生崩溃

四.objc中向一个nil对象发送消息将会发生什么?

在oc中向nil发送消息是完全有效的,只是在运行时不会有任何作用,如果一个方法返回值是一个对象,那么发送给nil的消息将返回0(nil),如果向一个nil对象发送消息,首先寻找对象的isa指针时就是0地址返回了,所以不会出现任何错误.

五.runtime如何实现weak变量的自动置nil

runtime对注册的类,会进行布局,对于weak对象会放入一个hash表中,用weak指向的对象内存地址作为key,当此对象的引用计数为0的时候会dealloc,假如weak指向的对象内存地址是a,那么就会以a为键,在这个weak表中搜索,找到所有以a为键的weak对象,从而设置为nil

六.runtime如何通过selector找到对应的IMP地址?

每一个类对象都有一个方法列表,方法列表中记录着方法名称.方法实现.参数类型,其实selector本质就是方法名称,通过这个方法名称就可以在方法列表中找到对应的方法实现

七.能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?

1.不能向编译后得到的类中增加实例变量,可以向运行时创建的类中添加实例变量

原因: (1)因为编译后的类已经注册在runtime中,类结构中的objc_ivar_list 实例变量的链表和instance_size实例变量的内存大小已经确定,同事runtime会调用class_setIvarLayout 或者class_setWeakIvarLayout来处理strong weak引用,所以不能向存在的类中添加实例变量

(2)运行时创建的的类可以添加实例变量,调用class_addIvar函数,但是得在调用objc_allocateClassPair之后,objc_registerClassPair之前,原因同上

八.kvo的实现原理?

当你观察一个对象时,一个新的类会被动态创建,这个类继承自该对象的原本的类,并重写了被观察属性的setter方法,重写setter方法会负责在调用原setter方法之前和之后,通知所有观察对象:值得更改,最后通过isa混写,把这个对象的isa指针(isa指针告诉runtime系统这个对象的类是什么)指向这个新创建的子类,对象就神奇的变成了新创建子类的实例

实现原理如下图:


九.谈谈你对kvc的理解

kvc可以通过key直接访问对象属性,或者给对象的属性赋值,这样可以在运行时动态访问或者改变对象的属性值

当调用setValue:属性值forKey:@"name"的代码时,底层执行机制如下:

1.程序优先调用set:属性值方法,代码通过setter方法完成设置,注意,这里的是指成员变量名,首字母大小写要符合kvc的命名规则

2.如果没有找到setName:方法,kvc机制会检查+(Bool)accessInstanceVariablesDiretly方法,有没有返回yes,默认是返回yes,如果重写了该方法切返回NO的话,那么这一步会直接执行setValue:forUnderfindKey:方法, 如果返回Yes,那么kvc机制会搜索该类里面有没有名为的成员变量,不论该变量是在类接口处定义,还是在类实现处定义,也无论用什么样的访问修饰符只要存在以命名的变量,kvc都可以对该变量赋值

3.如果该类即没有set方法,也没有成员变量,kvc机制会搜索_is的成员变量

4.和上面一样,如果该类即没有set:方法,也没有_和_is成员变量,kvc机制会继续搜索和is的成员变量,再给它赋值

5.如果以上列出的方法和成员变量都不存在,系统将执行setValue:forUnderfindKey:方法,默认是抛出异常.

十.Notification和KVO的区别

1.KVO提供一种机制,当指定的被观察对象属性被修改后,kvo会自动通知响应的观察者

2.通知:是一种广播机制,在事件发生的时候,通过通知中心对象,一个对象能够为所关心这个事件发生的对象发送消息,两者都是观察者模式,不同在于kvo是被观察者直接发送消息给观察者,是对象间的直接交互.通知则是两者都和通知中心对象交互,对象之间不知道彼此

3.本质区别,底层原理不一样,kvo基于runtime,通知则有个通知中心来进行通知

十一.如果让你设计一个通知中心,设计思路

1.创建通知中心单例类,并在里面有一个保存全局NSDictionary

2.对于注册通知的类,将注册通知名作为key,执行的方法和类,以及一些参数作为一个数组为值

3.发送通知可以调用通知中心,通过字典key(通知名)找到对应的类和方法进行调用传值

十二.atomic和nonatomic区别,以及作用?

atomic与nonatom的主要区别就是系统自动生成的getter/setter方法不一样 ,atomic系统自动生成的getter/setter方法会进行加锁操作,nonatomic系统自动生成的getter/setter方法不会进行加锁操作

atomic不是线程安全的

系统生成的getter/setter方法会进行加锁操作,注意:这个锁仅仅保证了getter和setter存取方法的线程安全.因为getter/setter方法有加锁的缘故,故在别的线程来读写这个属性之前,会先执行完当前操作

atomic可以保证多线程访问时候,对象是未被其他线程销毁(比如:如果当一个线程正在get或set时,又有另一个线程同时在进行release操作,可能会直接crash)

十三.说一下静态库和动态库之间的区别

静态库:以.a 和 .framework为文件后缀名。链接时会被完整的复制到可执行文件中,被多次使用就有多份拷贝。

动态库:以.tbd(之前叫.dylib) 和 .framework 为文件后缀名。链接时不复制,程序运行时由系统动态加载到内存,系统只加载一次,多个程序共用(如系统的UIKit.framework等),节省内存 

静态库.a 和 framework区别.a 主要是二进制文件,不包含资源,需要自己添加头文件.framework 可以包含头文件+资源信息

十四.遇到过BAD_ACCESS的错误吗?你是怎样调试的?

BAD_ACCESS 报错属于内存访问错误,会导致程序崩溃,错误的原因是访问了野指针(悬挂指针)。

常规操作如下:

设置全局断点快速定位问题代码所在行。

开启僵尸对象诊断
Analyze分析
重写object的respondsToSelector方法,现实出现EXEC_BAD_ACCESS前访问的最后一个object。
Xcode 7 已经集成了BAD_ACCESS捕获功能:Address Sanitizer。 用法如下:在配置中勾选✅Enable Address Sanitizer

十五.说一下iOS 中的APNS,远程推送原理?

Apple push Notification Service,简称 APNS,是苹果的远程消息推送,原理如下:
iOS 系统向APNS服务器请求手机端的deviceToken
App 接收到手机端的 deviceToken,然后传给 App 对应的服务器.
App 服务端需要发送推送消息时, 需要先通过 APNS 服务器
然后根据对应的 deviceToken 发送给对应的手机

十六.UITableView的优化

1.重用cell

2.缓存行高(在请求到数据的时候提前计算好行高,用字典缓存好高度)

3.加载网络图片,使用异步加载,并缓存,下载的图片根据显示大小切成合适大小的图,查看大图时再显示大图,服务端最好处理好大图和小图,延时加载,当滚动很快时避免频繁请求,可通过runloop设置defultMode状态下渲染请求

4.局部刷新,减少全局刷新

5.渲染,尽量不要使用透明图层,将cell的opaque值设为Yes,背景色和子View不要使用透明色,减少阴影渐变,圆角等

6.少用addSubview给cell动态添加子View,初始化时直接设置好,通过hidden控制显示隐藏,布局在初始化直接布局好,避免cell的重新布局

7.按需加载cell,滚动很快时,只加载范围内的cell,如果目标行与当前行相差超过指定行数,只在目标滚动范围的前后定制n行加载,按需加载,提高流畅性方法如下


8.遇到复杂界面,需要异步绘制,给自定义的cell添加draw方法,在方法中利用GCD异步绘制,或者直接重写drawRect方法,此外,绘制cell不建议使用UIView,建议使用CALayer,UIView的绘制是建立在CoreGraphic上的,使用的是cpu,CALayer使用的是core Animation,CPU.GPU通吃.由系统决定使用哪一个,view的绘制使用的自下向上的一层层的绘制,然后渲染layer处理的是Texure,利用GPU的Texure Cache和独立的浮点数计算单元加速纹理的处理,GPU不喜欢透明,所以绘图一定要弄成不透明,对于圆角和阴影截一个伪透明的小图绘制上去,在layer回调里一定只做绘图,不做计算

cell被重用时,内部绘制的内容并不会自动清除,因此需要调用setNeedsDisplay或者setNeedsDisplayLayInRect:方法

十七.离屏渲染

下面的情况或操作会引发离屏渲染:

1.为图层设置遮罩(layer.mask)
2.将图层的layer.masksToBounds/view.clipsToBounds属性设置为ture
3.将图层layer,allowsGroupOpacity属性设置为Yes和layer.opacity小于1.0
4.给图层设置阴影(layer.shadow)
5.为图层设置layer.shouldRasterize=Yes(光栅化)
6.具有layer.cornerRadius,layer.edgeAntialiasingMask,layer.allowsEdgeAntialiasing的图层(圆角,抗锯齿)
7.使用CGContext在drawRect:方法中绘制大部分情况会导致离屏渲染,甚至是一个空的实现

优化方案

圆角优化:使用CAShapeLayer和UIBezierPath设置圆角;直接中间覆盖一张为圆形的透明图片
shadow优化:使用shadowPath指定layer阴影效果路径,优化性能
使用异步进行layer渲染(Facebook开源异步绘制框架AsncDisplayKit)
设置layer的opaque值为Yes,减少复杂图层合成
尽量使用不包含透明(alpha)通道的图片资源
尽量设置layer的大小为整型值

十八.UIView和CALayer区别

1.UIView可以响应事件,CALayer不可以,UIView继承自UIResponder,在UIResponder中定义了处理各种事件的事件传递接口。而CALayer直接继承NSObject,并没有相应的处理事件接口。
2.一个CALayer的frame是由它的anchorPoint(锚点),position,bounds,和transform共同决定,而一个view的frame只是简单的返回layer的frame,同样view的center和bounds也是返回layer的一些属性
3..UIView主要是对显示内容的管理,而CALayer主要是侧重显示内容的绘制。UIView是CALayer的CALayerDelegate。
4.每一个view内部都有一个CALayer在背后提供内容的绘制和显示,并且UIView的尺寸样式都由内部的CALayer提供,两者都有树状层级结构,layer内部有subLayers,view内部有subviews。
5.两者最明显的区别是view可以接受并处理事件,而Layer不可以。View是Layer的代理Delegate。

十九.iOS应用程序生命周期

ios程序启动原理(过程):如下:


二十.view视图生命周期



























收起阅读 »

协程+Retrofit 让你的代码足够优雅

目标 简单起见,我们使用 Github 官方的 Api,查询官方返回的仓库列表。 如果你学习过官方 Paging Demo 的源码,会发现这份代码很熟悉,因为这份代码很大一部分来自这个Demo。 一、引入依赖 协程和 Retrofit 的版本:dependen...
继续阅读 »

目标


简单起见,我们使用 Github 官方的 Api,查询官方返回的仓库列表。


如果你学习过官方 Paging Demo 的源码,会发现这份代码很熟悉,因为这份代码很大一部分来自这个Demo。


一、引入依赖


协程和 Retrofit 的版本:

dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2"
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.0.0'
}

二、使用Retrofit


创建一个 interface

interface GithubApi {
@GET("search/repositories?sort=stars")
suspend fun searchRepos(
@Query("q") query: String,
@Query("page") page: Int,
@Query("per_page") itemsPerPage: Int
): RepoSearchResponse
}

和我们平时使用 Retrofit 有两点不一样:



  1. 需要在函数前加上 suspend 修饰符

  2. 直接使用我们需要返回的类型,不需要包上 Call<T> 或者 Observable<T>


RepoSearchResponse 是返回的数据:

data class RepoSearchResponse(
@SerializedName("total_count") val total: Int = 0,
@SerializedName("items") val items: List<Repo> = emptyList()
)

data class Repo(
@SerializedName("id") val id: Long,
@SerializedName("name") val name: String,
@SerializedName("full_name") val fullName: String,
@SerializedName("description") val description: String?,
@SerializedName("html_url") val url: String,
@SerializedName("stargazers_count") val stars: Int,
@SerializedName("forks_count") val forks: Int,
@SerializedName("language") val language: String?
)

之后的步骤就和我们平常使用 Retrofit 一致:



  1. 创建一个 OkHttpClient

  2. 创建一个 Retrofit

  3. 返回上面创建的接口


代码:

interface GithubApi {
//... 代码省略

companion object {
private const val BASE_URL = "https://api.github.com/"

fun createGithubApi(): GithubApi {
val logger = HttpLoggingInterceptor()
logger.level = HttpLoggingInterceptor.Level.BASIC

val client = OkHttpClient.Builder()
.addInterceptor(logger)
.sslSocketFactory(SSLSocketClient.getSSLSocketFactory(),
object : X509TrustManager {
override fun checkClientTrusted(
chain: Array<X509Certificate>,
authType: String
) {}
override fun checkServerTrusted(
chain: Array<X509Certificate>,
authType: String
) {}
override fun getAcceptedIssuers(): Array<X509Certificate> {
return arrayOf()
}
})
.hostnameVerifier(SSLSocketClient.getHostnameVerifier())
.build()
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(GithubApi::class.java)
}
}
}

因为接口是 Https 请求,所以需要加上忽略 SSL 的验证,其他都一样了。


三、使用协程去请求


初始化 RecyclerView 的代码就不放了,比较简单:

class MainActivity : AppCompatActivity() {
val scope = MainScope()
private val mAdapter by lazy {
MainAdapter()
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

// ... 初始化RecyclerView
fetchData()
}

private fun fetchData(){
scope.launch {
try {
val result = GithubApi.createGithubApi().searchRepos("Android", 0, 20)
if(result != null && !result.items.isNullOrEmpty()){
mAdapter.submitList(result.items)
}
}catch (e: Exception){
e.printStackTrace()
}
}
}

override fun onDestroy() {
super.onDestroy()
scope.cancel()
}
}

协程中最方便的还是省去切线程的步骤,用同步代码处理耗时的异步网络请求


需要注意的是,由于没有使用 LifecycleKTX 的扩展库,所以协程作用域的生命周期得我们自己去释放,在上面的代码中,我是在 onCreate 方法中启动了一个协程,然后在 onDestroy 方法中去取消了正在执行的任务,以防内存泄漏。


总结


协程 + Retrofit 的方便之处在于:使用同步代码处理异步的网络请求,减去 Retrofit 中网络回调或者 RxJava + Retrofit 的请求回调


作者:九心_
链接:https://www.jianshu.com/p/dd3a9323b81a
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Jetpack新成员,Paging3从吐槽到真香

各位小伙伴们大家早上好。 随着Android 11的正式发布,Jetpack家族也引入了许多新的成员。我之前有承诺过,对于新引入的App Startup、Hilt、Paging 3,我会分别写一篇文章进行介绍。 现在,关于App Start和Hilt的文章我都...
继续阅读 »

各位小伙伴们大家早上好。


随着Android 11的正式发布,Jetpack家族也引入了许多新的成员。我之前有承诺过,对于新引入的App Startup、Hilt、Paging 3,我会分别写一篇文章进行介绍。


现在,关于App Start和Hilt的文章我都已经写完了,请参考 Jetpack新成员,App Startup一篇就懂 和 Jetpack新成员,一篇文章带你玩转Hilt和依赖注入 。


那么本篇文章,我们要学习的自然就是Paging 3了。


Paging 3简介


Paging是Google推出的一个应用于Android平台的分页加载库。


事实上,Paging并不是现在才刚刚推出的,而是之前就已经推出过两个版本了。


但Paging 3和前面两个版本的变化非常大,甚至可以说是完全不同的东西了。所以即使你之前没有学习过Paging的用法也没有关系,把Paging 3当成是一个全新的库去学习就可以了。


我相信一定会有很多朋友在学习Paging 3的时候会产生和我相同的想法:本身Android上的分页功能并不难实现,即使没有Paging库我们也完全做得出来,但为什么Paging 3要把一个本来还算简单的功能设计得如此复杂呢?


是的,Paging 3很复杂,至少在你还不了解它的情况下就是如此。我在第一次学习Paging 3的时候就直接被劝退了,心想着何必用这玩意委屈自己呢,自己写分页功能又不是做不出来。


后来本着拥抱新技术的态度,我又去学习了一次Paging 3,这次算是把它基本掌握了,并且还在我的新开源项目 Glance 当中应用了Paging 3的技术。


如果现在再让我来评价一下Paging 3,那么我大概是经历了一个由吐槽到真香的过程。理解了Paging 3之后,你会发现它提供了一套非常合理的分页架构,我们只需要按照它提供的架构去编写业务逻辑,就可以轻松实现分页功能。我希望大家在看完这篇文章之后,也能觉得Paging 3香起来。


不过,本篇文章我不能保证它的易懂性。虽然很多朋友都觉得我写的文章简单易懂,但Paging 3的复杂性在于它关联了太多其他的知识,如协程、Flow、MVVM、RecyclerView、DiffUtil等等,如果你不能将相关联的这些知识都有所了解,那么想要掌握Paging 3就会更有难度。


另外,由于Paging 3是Google基于Kotlin协程全新重写的一个库,所以它主要是应用于Kotlin语言(Java也能用,但是会更加复杂),并且以后这样的库会越来越多,比如Jetpack Compose等等。如果你对于Kotlin还不太了解的话,可以去参郭霖的新书《第一行代码 Android 第3版》。


上手Paging 3


经过我自己的总结,我发现如果零散去介绍一些Paging 3的知识点是很难能掌握得了这个库的。最好的学习方式就是直接上手,用Paging 3去做一个项目,项目做完了,你也基本就掌握了。本篇文章中我们就会采用这种方式来学习。


另外,我相信大家之前应该都做过分页功能,正如我所说,这个功能并不难实现。但是现在,请你完全忘掉过去你所熟知的分页方案,因为它不仅对理解Paging 3没有帮助,反而在很大程度上会影响你对Paging 3的理解。


是的,不要想着去监听列表滑动事件,滑动到底部的时候发起一个网络请求加载下一页数据。Paging 3完全不是这么用的,如果你还保留着这种过去的实现思路,在学习Paging 3的时候会很受阻。


那么现在就让我们开始吧。


首先新建一个Android项目,这里我给它起名为Paging3Sample。


接下来,我们在build.gradle的dependencies当中添加必要的依赖库:

dependencies {
...
implementation 'androidx.paging:paging-runtime:3.0.0-beta01'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
}

注意虽然我刚才说,Paging 3是要和很多其他关联库结合到一起工作的,但是我们并不需要将这些关联库一一手动引入,引入了Paging 3之后,所有的关联库都会被自动下载下来。


另外这里还引入了Retrofit的库,因为待会我们会从网络上请求数据,并通过Paging 3进行分页展示。


那么在正式开始涉及Paging 3的用法之前,让我们先来把网络相关的代码搭建好,方便为Paging 3提供分页数据。


这里我准备采用GitHub的公开API来作为我们这个项目的数据源,请注意GitHub在国内虽然一般都是可以访问的,但有时接口并不稳定,如果你无法正常请求到数据的话,请自行科学上网。


我们可以尝试在浏览器中请求如下接口地址:

https://api.github.com/search/repositories?sort=stars&q=Android&per_page=5&page=1

这个接口表示,会返回GitHub上所有Android相关的开源库,以Star数量排序,每页返回5条数据,当前请求的是第一页。


服务器响应的数据如下,为了方便阅读,我对响应数据进行了简化:

{
"items": [
{
"id": 31792824,
"name": "flutter",
"description": "Flutter makes it easy and fast to build beautiful apps for mobile and beyond.",
"stargazers_count": 112819,
},
{
"id": 14098069,
"name": "free-programming-books-zh_CN",
"description": ":books: 免费的计算机编程类中文书籍,欢迎投稿",
"stargazers_count": 76056,
},
{
"id": 111583593,
"name": "scrcpy",
"description": "Display and control your Android device",
"stargazers_count": 44713,
},
{
"id": 12256376,
"name": "ionic-framework",
"description": "A powerful cross-platform UI toolkit for building native-quality iOS, Android, and Progressive Web Apps with HTML, CSS, and JavaScript.",
"stargazers_count": 43041,
},
{
"id": 55076063,
"name": "Awesome-Hacking",
"description": "A collection of various awesome lists for hackers, pentesters and security researchers",
"stargazers_count": 42876,
}
]
}

简化后的数据格式还是非常好理解的,items数组中记录了第一页包含了哪些库,其中name表示该库的名字,description表示该库的描述,stargazers_count表示该库的Star数量。


那么下面我们就根据这个接口来编写网络相关的代码吧,由于这部分都是属于Retrofit的用法,我会介绍的比较简略。


首先根据服务器响应的Json格式定义对应的实体类,新建一个Repo类,代码如下所示:

data class Repo(
@SerializedName("id") val id: Int,
@SerializedName("name") val name: String,
@SerializedName("description") val description: String?,
@SerializedName("stargazers_count") val starCount: Int
)

然后定义一个RepoResponse类,以集合的形式包裹Repo类:

class RepoResponse(
@SerializedName("items") val items: List<Repo> = emptyList()
)

接下来定义一个GitHubService用于提供网络请求接口,如下所示:

interface GitHubService {

@GET("search/repositories?sort=stars&q=Android")
suspend fun searchRepos(@Query("page") page: Int, @Query("per_page") perPage: Int): RepoResponse

companion object {
private const val BASE_URL = "https://api.github.com/"

fun create(): GitHubService {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(GitHubService::class.java)
}
}

}

这些都是Retrofit的标准用法,现在当调用searchRepos()函数时,Retrofit就会自动帮我们向GitHub的服务器接口发起一条网络请求,并将响应的数据解析到RepoResponse对象当中。


好了,现在网络相关的代码都已经准备好了,下面我们就开始使用Paging 3来实现分页加载功能。


Paging 3有几个非常关键的核心组件,我们需要分别在这几个核心组件中按部就班地实现分页逻辑。


首先最重要的组件就是PagingSource,我们需要自定义一个子类去继承PagingSource,然后重写load()函数,并在这里提供对应当前页数的数据。


新建一个RepoPagingSource继承自PagingSource,代码如下所示:

class RepoPagingSource(private val gitHubService: GitHubService) : PagingSource<Int, Repo>() {

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Repo> {
return try {
val page = params.key ?: 1 // set page 1 as default
val pageSize = params.loadSize
val repoResponse = gitHubService.searchRepos(page, pageSize)
val repoItems = repoResponse.items
val prevKey = if (page > 1) page - 1 else null
val nextKey = if (repoItems.isNotEmpty()) page + 1 else null
LoadResult.Page(repoItems, prevKey, nextKey)
} catch (e: Exception) {
LoadResult.Error(e)
}
}

override fun getRefreshKey(state: PagingState<Int, Repo>): Int? = null

}

这段代码并不长,但却需要好好解释一下。


在继承PagingSource时需要声明两个泛型类型,第一个类型表示页数的数据类型,我们没有特殊需求,所以直接用整型就可以了。第二个类型表示每一项数据(注意不是每一页)所对应的对象类型,这里使用刚才定义的Repo。


然后在load()函数当中,先通过params参数得到key,这个key就是代表着当前的页数。注意key是可能为null的,如果为null的话,我们就默认将当前页数设置为第一页。另外还可以通过params参数得到loadSize,表示每一页包含多少条数据,这个数据的大小我们可以在稍后设置。


接下来调用刚才在GitHubService中定义的searchRepos()接口,并把page和pageSize传入,从服务器获取当前页所对应的数据。


最后需要调用LoadResult.Page()函数,构建一个LoadResult对象并返回。注意LoadResult.Page()函数接收3个参数,第一个参数传入从响应数据解析出来的Repo列表即可,第二和第三个参数分别对应着上一页和下一页的页数。针对于上一页和下一页,我们还额外做了个判断,如果当前页已经是第一页或最后一页,那么它的上一页或下一页就为null。


这样load()函数的作用就已经解释完了,可能你会发现,上述代码还重写了一个getRefreshKey()函数。这个函数是Paging 3.0.0-beta01版本新增的,以前的alpha版中并没有。它是属于Paging 3比较高级的用法,我们本篇文章涉及不到,所以直接返回null就可以了。


PagingSource相关的逻辑编写完成之后,接下来需要创建一个Repository类。这是MVVM架构的一个重要组件,还不了解的朋友可以去参考《第一行代码 Android 第3版》第15章的内容。

object Repository {

private const val PAGE_SIZE = 50

private val gitHubService = GitHubService.create()

fun getPagingData(): Flow<PagingData<Repo>> {
return Pager(
config = PagingConfig(PAGE_SIZE),
pagingSourceFactory = { RepoPagingSource(gitHubService) }
).flow
}

}

这段代码虽然很短,但是却不易理解,因为用到了协程的Flow。我无法在这里展开解释Flow是什么,你可以简单将它理解成协程中对标RxJava的一项技术。


当然这里也没有用到什么复杂的Flow技术,正如你所见,上面的代码很简短,相比于理解,这更多是一种固定的写法。


我们定义了一个getPagingData()函数,这个函数的返回值是Flow<PagingData<Repo>>,注意除了Repo部分是可以改的,其他部分都是固定的。


在getPagingData()函数当中,这里创建了一个Pager对象,并调用.flow将它转换成一个Flow对象。在创建Pager对象的时候,我们指定了PAGE_SIZE,也就是每页所包含的数据量。又指定了pagingSourceFactory,并将我们自定义的RepoPagingSource传入,这样Paging 3就会用它来作为用于分页的数据源了。


将Repository编写完成之后,我们还需要再定义一个ViewModel,因为Activity是不可以直接和Repository交互的,要借助ViewModel才可以。新建一个MainViewModel类,代码如下所示:

class MainViewModel : ViewModel() {

fun getPagingData(): Flow<PagingData<Repo>> {
return Repository.getPagingData().cachedIn(viewModelScope)
}

}

代码很简单,就是调用了Repository中定义的getPagingData()函数而已。但是这里又额外调用了一个cachedIn()函数,这是用于将服务器返回的数据在viewModelScope这个作用域内进行缓存,假如手机横竖屏发生了旋转导致Activity重新创建,Paging 3就可以直接读取缓存中的数据,而不用重新发起网络请求了。


写到这里,我们的这个项目已经完成了一大半了,接下来开始进行界面展示相关的工作。


由于Paging 3是必须和RecyclerView结合使用的,下面我们定义一个RecyclerView的子项布局。新建repo_item.xml,代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:orientation="vertical">

<TextView
android:id="@+id/name_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:maxLines="1"
android:ellipsize="end"
android:textColor="#5194fd"
android:textSize="20sp"
android:textStyle="bold" />

<TextView
android:id="@+id/description_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:maxLines="10"
android:ellipsize="end" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="end"
tools:ignore="UseCompoundDrawables">

<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="5dp"
android:src="@drawable/ic_star"
tools:ignore="ContentDescription" />

<TextView
android:id="@+id/star_count_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical" />

</LinearLayout>

</LinearLayout>

这个布局中使用到了一个图片资源,可以到本项目的源码中去获取,源码地址见文章最底部。


接下来定义RecyclerView的适配器,但是注意,这个适配器也比较特殊,必须继承自PagingDataAdapter,代码如下所示:

class RepoAdapter : PagingDataAdapter<Repo, RepoAdapter.ViewHolder>(COMPARATOR) {

companion object {
private val COMPARATOR = object : DiffUtil.ItemCallback<Repo>() {
override fun areItemsTheSame(oldItem: Repo, newItem: Repo): Boolean {
return oldItem.id == newItem.id
}

override fun areContentsTheSame(oldItem: Repo, newItem: Repo): Boolean {
return oldItem == newItem
}
}
}

class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val name: TextView = itemView.findViewById(R.id.name_text)
val description: TextView = itemView.findViewById(R.id.description_text)
val starCount: TextView = itemView.findViewById(R.id.star_count_text)
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.repo_item, parent, false)
return ViewHolder(view)
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val repo = getItem(position)
if (repo != null) {
holder.name.text = repo.name
holder.description.text = repo.description
holder.starCount.text = repo.starCount.toString()
}
}

}

相比于一个传统的RecyclerView Adapter,这里最特殊的地方就是要提供一个COMPARATOR。因为Paging 3在内部会使用DiffUtil来管理数据变化,所以这个COMPARATOR是必须的。如果你以前用过DiffUtil的话,对此应该不会陌生。


除此之外,我们并不需要传递数据源给到父类,因为数据源是由Paging 3在内部自己管理的。同时也不需要重写getItemCount()函数了,原因也是相同的,有多少条数据Paging 3自己就能够知道。


其他部分就和普通的RecyclerView Adapter没什么两样了,相信大家都能够看得明白。


接下来就差最后一步了,让我们把所有的一切都集成到Activity当中。


修改activity_main.xml布局,在里面定义一个RecyclerView和一个ProgressBar:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />

</FrameLayout>

然后修改MainActivity中的代码,如下所示:

class MainActivity : AppCompatActivity() {

private val viewModel by lazy { ViewModelProvider(this).get(MainViewModel::class.java) }

private val repoAdapter = RepoAdapter()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
val progressBar = findViewById<ProgressBar>(R.id.progress_bar)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = repoAdapter
lifecycleScope.launch {
viewModel.getPagingData().collect { pagingData ->
repoAdapter.submitData(pagingData)
}
}
repoAdapter.addLoadStateListener {
when (it.refresh) {
is LoadState.NotLoading -> {
progressBar.visibility = View.INVISIBLE
recyclerView.visibility = View.VISIBLE
}
is LoadState.Loading -> {
progressBar.visibility = View.VISIBLE
recyclerView.visibility = View.INVISIBLE
}
is LoadState.Error -> {
val state = it.refresh as LoadState.Error
progressBar.visibility = View.INVISIBLE
Toast.makeText(this, "Load Error: ${state.error.message}", Toast.LENGTH_SHORT).show()
}
}
}
}

}

这里最重要的一段代码就是调用了RepoAdapter的submitData()函数。这个函数是触发Paging 3分页功能的核心,调用这个函数之后,Paging 3就开始工作了。


submitData()接收一个PagingData参数,这个参数我们需要调用ViewModel中返回的Flow对象的collect()函数才能获取到,collect()函数有点类似于Rxjava中的subscribe()函数,总之就是订阅了之后,消息就会源源不断往这里传。


不过由于collect()函数是一个挂起函数,只有在协程作用域中才能调用它,因此这里又调用了lifecycleScope.launch()函数来启动一个协程。


其他地方应该就没什么需要解释的了,都是一些传统RecyclerView的用法,相信大家都能看得懂。


好了,这样我们就把整个项目完成了,在正式运行项目之前,别忘了在你的AndroidManifest.xml文件中添加网络权限:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.paging3sample">

<uses-permission android:name="android.permission.INTERNET" />
...

</manifest>



可以看到,GitHub上Android相关的开源库已经成功显示出来了。并且你可以不断往下滑,Paging 3会自动加载更多的数据,仿佛让你永远也滑不到头一样。


如次一来,使用Paging 3来进行分页加载的效果也就成功完成了。


总结一下,相比于传统的分页实现方案,Paging 3将一些琐碎的细节进行了隐藏,比如你不需要监听列表的滑动事件,也不需要知道知道何时应该加载下一页的数据,这些都被Paging 3封装掉了。我们只需要按照Paging 3搭建好的框架去编写逻辑实现,告诉Paging 3如何去加载数据,其他的事情Paging 3都会帮我们自动完成。


在底部显示加载状态


根据Paging 3的设计,其实我们理论上是不应该在底部看到加载状态的。因为Paging 3会在列表还远没有滑动到底部的时候就提前加载更多的数据(这是默认属性,可配置),从而产生一种好像永远滑不到头的感觉。


然而凡事总有意外,比如说当前的网速不太好,虽然Paging 3会提前加载下一页的数据,但是当滑动到列表底部的时候,服务器响应的数据可能还没有返回,这个时候就应该在底部显示一个正在加载的状态。


另外,如果网络条件非常糟糕,还可能会出现加载失败的情况,此时应该在列表底部显示一个重试按钮。


那么接下来我们就来实现这个功能,从而让项目变得更加完善。


创建一个footer_item.xml布局,用于显示加载进度条和重试按钮:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp">

<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />

<Button
android:id="@+id/retry_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Retry" />

</FrameLayout>

然后创建一个FooterAdapter来作为RecyclerView的底部适配器,注意它必须继承自LoadStateAdapter,如下所示:

class FooterAdapter(val retry: () -> Unit) : LoadStateAdapter<FooterAdapter.ViewHolder>() {

class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val progressBar: ProgressBar = itemView.findViewById(R.id.progress_bar)
val retryButton: Button = itemView.findViewById(R.id.retry_button)
}

override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.footer_item, parent, false)
val holder = ViewHolder(view)
holder.retryButton.setOnClickListener {
retry()
}
return holder
}

override fun onBindViewHolder(holder: ViewHolder, loadState: LoadState) {
holder.progressBar.isVisible = loadState is LoadState.Loading
holder.retryButton.isVisible = loadState is LoadState.Error
}

}

这仍然是一个非常简单的Adapter,需要注意的地方大概只有两点。


第一点,我们使用Kotlin的高阶函数来给重试按钮注册点击事件,这样当点击重试按钮时,构造函数中传入的函数类型参数就会被回调,我们待会将在那里加入重试逻辑。


第二点,在onBindViewHolder()中会根据LoadState的状态来决定如何显示底部界面,如果是正在加载中那么就显示加载进度条,如果是加载失败那么就显示重试按钮。


最后,修改MainActivity中的代码,将FooterAdapter集成到RepoAdapter当中:

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
...
recyclerView.adapter = repoAdapter.withLoadStateFooter(FooterAdapter { repoAdapter.retry() })
...
}

}

代码非常简单,只需要改动一行,调用RepoAdapter的withLoadStateFooter()函数即可将FooterAdapter集成到RepoAdapter当中。


另外注意这里使用Lambda表达式来作为传递给FooterAdapter的函数类型参数,在Lambda表示式中,调用RepoAdapter的retry()函数即可重新加载。


这样我们就把底部显示加载状态的功能完成了,现在来测试一下吧,效果如下图所示。




可以看到,首先我在设备上开启了飞行模式,这样当滑动到列表底部时就会显示重试按钮。


然后把飞行模式关闭,并点击重试按钮,这样加载进度条就会显示出来,并且成功加载出新的数据了。


最后


本文到这里就结束了。


不得不说,我在文章中讲解的这些知识点仍然只是Paging 3的基本用法,还有许多高级用法文中并没有涵盖。当然,这些基本用法也是最最常用的用法,所以如果你并不打算成为Paging 3大师,掌握文中的这些知识点就已经足够应对日常的开发工作了。


如果你还想要进一步进阶学习Paging 3,可以参考Google官方的Codelab项目,地址是:

https://developer.android.com/codelabs/android-paging


我们刚才一起编写的Paging3Sample项目其实就是从Google官方的Codelab项目演化而来的,我根据自己的理解重写了这个项目并进行了一定的简化。直接学习原版项目,你将能学到更多的知识。


最后,如果你需要获取Paging3Sample项目的源码,请访问以下地址:

https://github.com/guolindev/Paging3Sample

作者:飞鱼_9d08
链接:https://www.jianshu.com/p/588562fbd19d
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Android 常见的错误日志及相应的解决方案总结

之前整理过一些关于常见的错误日志,基于生产的bug日志系统,我这边会不间断的更新错误日志及相应的解决方案,抛砖引玉(PS:也许解决的方法有点菜,希望大家能给出更优的解决方案及意见反馈,非常欢迎,相互学习共同进步) android.view.WindowMana...
继续阅读 »

之前整理过一些关于常见的错误日志,基于生产的bug日志系统,我这边会不间断的更新错误日志及相应的解决方案,抛砖引玉(PS:也许解决的方法有点菜,希望大家能给出更优的解决方案及意见反馈,非常欢迎,相互学习共同进步)


android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?

at android.view.ViewRootImpl.setView(ViewRootImpl.java:635)
at android.view.ColorViewRootImpl.setView(ColorViewRootImpl.java:60)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:321)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:86)
at android.widget.PopupWindow.invokePopup(PopupWindow.java:1262)
at android.widget.PopupWindow.showAsDropDown(PopupWindow.java:1110)
at android.widget.PopupWindow.showAsDropDown(PopupWindow.java:1069)

以上bug出现的原因是因为PopupWindow需要依附在一个创建好的Activity上,那么出现这个异常就说明此时你的Activity还没有创建好,出现这种情况,很可能是在onCreate()或者是onStart()中调用导致的。

下面有两种方法可以解决这个问题:



方法一:重载Activity的onWindowFocusChanged方法,然后在里面实现相应的逻辑如下:

public void onWindowFocusChanged(boolean hasFocus) {  
super.onWindowFocusChanged(hasFocus);
if(hasFocus) {
//执行PopupWindow相应的操作
}
}

下面给大家看下这个方法的源码,有兴趣的小伙伴可以看看

 /**
* Called when the current {@link Window} of the activity gains or loses
* focus. This is the best indicator of whether this activity is visible
* to the user. The default implementation clears the key tracking
* state, so should always be called.
*
* <p>Note that this provides information about global focus state, which
* is managed independently of activity lifecycles. As such, while focus
* changes will generally have some relation to lifecycle changes (an
* activity that is stopped will not generally get window focus), you
* should not rely on any particular order between the callbacks here and
* those in the other lifecycle methods such as {@link #onResume}.
*
* <p>As a general rule, however, a resumed activity will have window
* focus... unless it has displayed other dialogs or popups that take
* input focus, in which case the activity itself will not have focus
* when the other windows have it. Likewise, the system may display
* system-level windows (such as the status bar notification panel or
* a system alert) which will temporarily take window input focus without
* pausing the foreground activity.
*
* @param hasFocus Whether the window of this activity has focus.
*
* @see #hasWindowFocus()
* @see #onResume
* @see View#onWindowFocusChanged(boolean)
*/
public void onWindowFocusChanged(boolean hasFocus) {
}


方法二:上面的那种方法是需要实现Activity的一个方法并在方法中做操作,一般我们在项目中会在一些逻辑里面showPopupWindow或者其他的,那这样就会影响一些,然后我们就针对这个源码,追溯一下会发现另外一个方法:hasWindowFocus

/**
* Returns true if this activity's <em>main</em> window currently has window focus.
* Note that this is not the same as the view itself having focus.
*
* @return True if this activity's main window currently has window focus.
*
* @see #onWindowAttributesChanged(android.view.WindowManager.LayoutParams)
*/
public boolean hasWindowFocus() {
Window w = getWindow();
if (w != null) {
View d = w.getDecorView();
if (d != null) {
return d.hasWindowFocus();
}
}
return false;
}

查看上面的源码,我们会发现,我们可以直接使用hasWindowFocus来判断当前的Activity有没有创建好,再去做其他操作;以上就是这个错误日志相应的解决方案,如果还有其他的希望大家补充。


java.lang.IllegalArgumentException: You cannot start a load for a destroyed activity


这个问题是在使用Glide的时候生产上面爆出来的,如果遇到其他相似的错误也可以试一下,以下有两种解决方案:



方法一:参考博文

在使用Glide的地方加上这个判断;Util是系统自带的;

if(Util.isOnMainThread()) {
Glide.with(AppUtil.getContext()).load``(R.mipmap.iclunch).error(R.mipmap.cuowu).into(imageView);
}

在使用的Glide的界面的生命周期onDestroy中添加如下代码:

@Override
protected void onDestroy() {
super.onDestroy();
if(Util.isOnMainThread()) {
Glide.with(this).pauseRequests();
}
}

上面Destroy中with(this),改成with(AppUtil.getContext());

不然会报: java.lang.IllegalStateException: Activity has been destroyed

扩展:

Glide.with(AppUtil.getContext()).resumeRequests()和 Glide.with(AppUtil.getContext()).pauseRequests()的区别:

1.当列表在滑动的时候,调用pauseRequests()取消请求;

2.滑动停止时,调用resumeRequests()恢复请求;

另外Glide.clear():当你想清除掉所有的图片加载请求时,这个方法可以用到。

ListPreloader:如果你想让列表预加载的话,可以试试这个类。

请记住一句话:不要再非主线程里面使用Glide加载图片,如果真的使用了,请把context参数换成getApplicationContext;



方法二:使用Activity提供的isFinishing和isDestroyed方法,来判断当前的Activity是不是已经销毁了或者说正在finishing,下面贴出来相应的源码:

/**
* Check to see whether this activity is in the process of finishing,
* either because you called {@link #finish} on it or someone else
* has requested that it finished. This is often used in
* {@link #onPause} to determine whether the activity is simply pausing or
* completely finishing.
*
* @return If the activity is finishing, returns true; else returns false.
*
* @see #finish
*/
public boolean isFinishing() {
return mFinished;
}

/**
* Returns true if the final {@link #onDestroy()} call has been made
* on the Activity, so this instance is now dead.
*/
public boolean isDestroyed() {
return mDestroyed;
}

附上项目相应的源码,希望有所帮助:

final WeakReference<ImageView> imgBankLogoWeakReference = new WeakReference<>(imgBankLogo);
final WeakReference<ImageView> imgBankBgWeakReference = new WeakReference<>(imgBankBg);
ImageView imgBankLogoTarget = imgBankLogoWeakReference.get();
ImageView imgBankBgTarget = imgBankBgWeakReference.get();
if (imgBankLogoTarget != null && imgBankBgTarget != null) {
if (!(isFinishing() || isDestroyed())) {
Glide.with(xxx.this).load(_bankCardInfo.getBankInfo().getBankLogo())
.centerCrop().into(imgBankLogoTarget);
Glide.with(xxx.this).load(_bankCardInfo.getBankInfo().getBankBg())
.centerCrop().into(imgBankBgTarget);
}
}

java.lang.IllegalStateException: Could not find a method OnButtonClick(View) in the activity class android.view.ContextThemeWrapper for onClick handler on view class android.widget.ImageView with id 'img_apply_result'
at android.view.View$1.onClick(View.java:4061)
at android.view.View.performClick(View.java:4848)
at android.view.View$PerformClick.run(View.java:20262)
at android.os.Handler.handleCallback(Handler.java:815)
at android.os.Handler.dispatchMessage(Handler.java:104)
at android.os.Looper.loop(Looper.java:194)
at android.app.ActivityThread.main(ActivityThread.java:5714)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:984)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:779)
Caused by: java.lang.NoSuchMethodException: OnButtonClick [class android.view.View]
at java.lang.Class.getMethod(Class.java:664)
at java.lang.Class.getMethod(Class.java:643)
at android.view.View$1.onClick(View.java:4054)


以上的错误日志出现的有点low,但是呢有时候部分人还是容易忽略:我们一般在Activity或fragment等Java代码中使用资源文件时,例如:在Java代码中对一个Imageview附一张图片,我们不能img.setImageResource(图片相应的资源ID);需要img.setImageResource(context.getResources().getDrawable(图片相应的资源ID));需要先获取文件资源,再去拿图片,但是刚刚写的那个方法现在已经过时了,下面我贴出Google官方给出的最新的方法img.setImageDrawable(ContextCompat.getDrawable(context, imgResId));其实setImageDrawable是最省内存高效的,如果担心图片过大或者图片过多影响内存和加载效率,可以自己解析图片然后通过调用setImageDrawable方法进行设置


java.lang.NoClassDefFoundError: android.app.AppOpsManager


Appops是Application Operations的简称,是关于应用权限管理的一套方案,但这里的应用指的是系统应用,这些API不对第三方应用开放。Appops的两个重要组成部分是AppOpsManager和AppOpsService,它们是典型的客户端和服务端设计,通过Binder跨进程调用。AppOpsManager提供标准的API供APP调用,但google有明确说明,大部分只针对系统应用。AppOpsService是做最终检查的系统服务,它的注册名字是appops, 应用可以类似于

mAppOps=(AppOpsManager)getContext().getSystemService(Context.APP_OPS_SERVICE);的方式来获取这个服务。


解决方法:这个api是在19新加入的,所以要注意加个判断,参考项目代码如下:



判断是否开启通知权限(解释比较好的博客推荐

private boolean isNotificationEnabled(Context context) {

String CHECK_OP_NO_THROW = "checkOpNoThrow";
String OP_POST_NOTIFICATION = "OP_POST_NOTIFICATION";
if (Build.VERSION.SDK_INT < 19) {
return true;
}
try {
AppOpsManager mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
ApplicationInfo appInfo = context.getApplicationInfo();
String pkg = context.getApplicationContext().getPackageName();
int uid = appInfo.uid;
Class appOpsClass = null;
/* Context.APP_OPS_MANAGER */
appOpsClass = Class.forName(AppOpsManager.class.getName());
Method checkOpNoThrowMethod = appOpsClass.getMethod(CHECK_OP_NO_THROW, Integer.TYPE, Integer.TYPE,
String.class);
Field opPostNotificationValue = appOpsClass.getDeclaredField(OP_POST_NOTIFICATION);

int value = (Integer) opPostNotificationValue.get(Integer.class);
return ((Integer) checkOpNoThrowMethod.invoke(mAppOps, value, uid, pkg) == AppOpsManager.MODE_ALLOWED);

} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoClassDefFoundError e) {
e.printStackTrace();
}
return false;
}

java.lang.SecurityException: getDeviceId: Neither user 10185 nor current process has android.permission.READ_PHONE_STATE.


这里的getDeviceId可能是获取系统状态或内容的操作,需要授予android.permission.READ_PHONE_STATE 权限,首先我们来看一下危险权限组



我们会发现android.permission.READ_PHONE_STATE 这个权限在PHONE组里面,在Android M版本及以后,当你的应用运行在Android6.0系统上如果设置targetSdkVersion小于23的时候,它也会默认采用以前的权限管理机制,当你的targetSdkVersion大于等于23的时候且在Andorid6.0(M)系统上,它会采用新的这套权限管理机制。相关动态权限爬坑这块可以看一下之前的博文(传送门)

,当你配置了targetSdkVersion>=23时,默认第一次安装会打开android.permission.READ_PHONE_STATE这个权限,部分手机亲测,那样依旧可以获取getDeviceId,但这个权限是可见的,用户在后续是可以关闭的。当用户关闭了这个权限,下次进来会动态弹出授权页面提醒用户授权,如果用户依旧关闭权限将获取不到DeviceId。但是国产手机的各种自定义导致部分手机会出现动态权限返回0,(PS:当用户禁止了权限,返回回调还是为已授权,例如:OPPO meizu等兼容),这样就尴尬了,如果我们拿到用户已经授权(但实际上是禁止的)就去调用

TelephonyManager tm = (TelephonyManager) getApplicationContext()
.getSystemService(Context.TELEPHONY_SERVICE);
_clientInfo.setDeviceId(tm.getDeviceId());

就会闪退,目前这边处理的思路为:第一次如果拿到就放在SharedPreferences里面存起来,当下次用户再次关闭权限也不用担心报错;


java.util.ConcurrentModificationExceptionat java.util.ArrayList$ArrayListIterator.next(ArrayList.java:578)
at com.google.gson.DefaultTypeAdapters$CollectionTypeAdapter.serialize(DefaultTypeAdapters.java:637)
at com.google.gson.DefaultTypeAdapters$CollectionTypeAdapter.serialize(DefaultTypeAdapters.java:624)
at com.google.gson.JsonSerializationVisitor.findAndInvokeCustomSerializer(JsonSerializationVisitor.java:184)
at com.google.gson.JsonSerializationVisitor.visitUsingCustomHandler(JsonSerializationVisitor.java:160)
at com.google.gson.ObjectNavigator.accept(ObjectNavigator.java:101)
at com.google.gson.JsonSerializationContextDefault.serialize(JsonSerializationContextDefault.java:62)
at com.google.gson.JsonSerializationContextDefault.serialize(JsonSerializationContextDefault.java:53)
at com.google.gson.Gson.toJsonTree(Gson.java:220)
at com.google.gson.Gson.toJson(Gson.java:260)
at com.google.gson.Gson.toJson(Gson.java:240)



在ArrayList.addAll()中对传进来的参数没有做null判断,于是,在调用collection.toArray()函数的时候就抛异常了,activity就崩溃了


在使用ArrayList.addAll()的时候一定要注意传入的参数会不会出现为null的情况,如果有,那么我们可以做以下判断

if (collection!= null)
mInfoList.addAll(Collection<? extends E> collection);

如果为null,就不执行下面的了,我们也不能确保是不是存在null的情况,所以为了确保不会出错,在前面加个判断是一个有经验的程序员该做的。以上错误日志的原因,可以看下源码大家就可以理解了:(这个问题虽小但容易忽略,希望各位注意)

/**
* Appends all of the elements in the specified collection to the end of
* this list, in the order that they are returned by the
* specified collection's Iterator. The behavior of this operation is
* undefined if the specified collection is modified while the operation
* is in progress. (This implies that the behavior of this call is
* undefined if the specified collection is this list, and this
* list is nonempty.)
*
* @param c collection containing elements to be added to this list
* @return <tt>true</tt> if this list changed as a result of the call
* @throws NullPointerException if the specified collection is null
*/
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacity(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}

android.content.ActivityNotFoundException: Unable to find explicit activity class {com.android.browser/com.android.browser.BrowserActivity}; have you declared this activity in your AndroidManifest.xml?


以上错误日志出现的背景是调用系统自带的浏览器出现的,原因是因为部分手机设备商更改Android原生自带的com.android.browser/com.android.browser.BrowserActivity自己弄了一个其他的,例如,生产就出现一款手机 HTC 802t,这款手机自带浏览器的代码包名为:com.htc.sense.browser,看到这块是不是想吐槽一下,所以说如果直接写以下代码,就会出现以上错误日志:

 Intent intent = new Intent();
intent.setAction("android.intent.action.VIEW");
intent.addCategory(Intent.CATEGORY_BROWSABLE);
Uri contentUri = Uri.parse(_versionInfo.getAppUrl());
intent.setData(contentUri);
intent.setComponent(new ComponentName("com.android.browser", "com.android.browser.BrowserActivity"));


解决方案(PS:获取系统安装的所有的浏览器应用 过滤):

Intent intent = new Intent();
intent.setAction("android.intent.action.VIEW");
intent.addCategory(Intent.CATEGORY_BROWSABLE);
Uri contentUri = Uri.parse(_versionInfo.getAppUrl());
intent.setData(contentUri);
// HTC com.htc.sense.browser
List<ResolveInfo> resolveInfos = context.getPackageManager().queryIntentActivities(intent,
PackageManager.MATCH_DEFAULT_ONLY);//通过查询,获得所有ResolveInfo对象.
for (ResolveInfo resolveInfo : resolveInfos) {
browsers.add(resolveInfo.activityInfo.packageName);
System.out.println(resolveInfo.activityInfo.packageName);
}
if (browsers.contains("com.android.browser")) {
intent.setComponent(new ComponentName("com.android.browser", "com.android.browser.BrowserActivity"));
}
context.startActivity(intent);

android.os.FileUriExposedException/NullPointerException: Attempt to invoke virtual method 'java.lang.String android.net.Uri.getPath()' on a null object reference:


上述错误日志是Android 7.0应用间共享文件(FileProvider)兼容的问题,后续会出一篇博文来讲解:

下面提供代码:

/**
* 适配7.0及以上
*
* @param context
* @param file
* @return
*/
private static Uri getUriForFile(Context context, File file) {
if (context == null || file == null) {
throw new NullPointerException();
}
Uri uri;
if (Build.VERSION.SDK_INT >= 24) {
uri = FileProvider.getUriForFile(context.getApplicationContext(), "xxx.fileprovider", file);
} else {
uri = Uri.fromFile(file);
}
return uri;
}

AndroidManifest中配置provider:

<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.crfchina.market.fileprovider"
android:exported="false"
android:grantUriPermissions="true" >
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>

下面是很久之前的备忘的,也贴出来给大家分享一下。可能涉及到其他博文的内容,如有发现,麻烦私信,我后续加上 ……


android java.net.UnknownHostException: Unable to resolve host "...": No address associated 错误



解决方法:




  • (1)手机3G或者WIFI没有开启



  • (2).Manifest文件没有标明网络访问权限

    如果确认网络已经正常连接并且还是出这种错误的话,那么请看下你的Manifest文件是否标明应用需要网络访问权限,如果没标明的话,也访问不了网络,也会造成这种情况的.


    //网络访问权限


    <uses-permission android:name="android.permission.INTERNET" />




java.lang.NullPointerException: missing IConnectivityManager

at com.android.internal.util.Preconditions.checkNotNull(Preconditions.java:52)
at android.net.ConnectivityManager.<init>(ConnectivityManager.java:919)
at android.app.ContextImpl$11.createService(ContextImpl.java:387)
at android.app.ContextImpl$ServiceFetcher.getService(ContextImpl.java:278)
at android.app.ContextImpl.getSystemService(ContextImpl.java:1676)
at android.content.ContextWrapper.getSystemService(ContextWrapper.java:540)
at com.crfchina.market.util.NetUtil.getNetworkState(NetUtil.java:28)


错误日志产生原因:



Android里面内存泄漏问题最突出的就是Activity的泄漏,而泄漏的根源大多在于单例的使用,也就是一个静态实例持有了Activity的引用。静态变量的生命周期与应用(Application)是相同的,而Activity生命周期通常比它短,也就会造成在Activity生命周期结束后,还被引用导致无法被系统回收释放。

生成静态引用内存泄漏可能有两种情况:

1. 应用级:应用程序代码实现的单例没有很好的管理其生命周期,导致Activity退出后仍然被引用。
2. 系统级:Android系统级的实现的单例,被应用不小心错误调用(当然你也可以认为是系统层实现地不太友好)。

这个主要讲下系统级的情况,这样的情况可能也有很多,举个最近发现的问题ConnectivityManager。

通常我们获取系统服务时采用如下方式:

context.getSystemService()

在Android6.0系统上,如果这里的Context如果是Activity的实例,那么即使你什么也不干也会造成内存泄漏。

这个Context在ConnectivityManager 创建时传入,这个Context在StaticOuterContextServiceFetcher中由ContextImpl对象转换为OuterContext,与就是Activity对象,所以最终ConnectivityManager的单实例持有了Activity的实例引用。这样即使Activity退出后仍然无法释放,导致内存泄漏。

这个问题仅在6.0上出现,在5.1上ConnectivityManager实现为单例但不持有Context的引用,在5.0有以下版本ConnectivityManager既不为单例,也不持有Context的引用。

其他服务没认真研究,不确定有没有这个问题。不过为了避免类似的情况发生,

最好的解决办法就是:



解决方案:



获取系统服务getSystemService时使用ApplicationContext

context.getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);


java.lang.IllegalArgumentException: View not attached to window manager



错误日志产生原因:



在延时线程里调用了ProgressDialog.dismiss,但此时主Activity已经destroy了。于是应用崩溃,我写了一个 SafeProgressDialog 来避免这个问题,主要原理是覆写dismiss方法,在ProgressDialog.dismiss之前判断Activity是否存在。



解决方案:

class SafeProgressDialog extends ProgressDialog
{
Activity mParentActivity;
public SafeProgressDialog(Context context)
{
super(context);
mParentActivity = (Activity) context;
}

@Override
public void dismiss()
{
if (mParentActivity != null && !mParentActivity.isFinishing())
{
super.dismiss(); //调用超类对应方法
}
}
}

Android.support.v4.app.Fragment$InstantiationException: Unable to instantiate fragment com.test.testFragment: make sure class name exists, is public, and has an empty constructor that is public



错误日志产生原因及解决方案:



根据报错提示 “make sure class name exists, is public, and has an empty constructor that is public” ,若Fragement定义有带参构造函数,则一定要定义public的默认的构造函数。即可解决此问题。

除了他说的public.还有一个就是弄个空的构造函数。

例如我是这样定义了我的fragment。带有了构造函数

public TestFragment(int index){
mIndex = index;
}

然后我添加了一个这样的构造函数

public TestFragment(){
}

java.lang.IllegalStateException: Unable to get package info for com.crfchina.market; is package not installed?



错误日志产生原因:



简单的卸载app 没有卸载干净,然后再次运行,当dalvik重新安装。apk文件并试图重用以前的活动从同一个包




好了目前就总结这么多,后续还会继续更新补充!毕竟太长也没有人愿意耐下心去看,以上也是曾经遇到过坑,希望有遇到的兄弟能从中受益!欢迎大家贴一些内容作为补充,相互学习共同进步……



作者:大荣言午
链接:https://www.jianshu.com/p/dd9714beb7ea
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

错过了金三银四,还不赶紧准备金九银十?这份Android大厂面试大纲静下心应对,九月就是你的战场!

感悟这个世界有一个“二八原则”在好多地方都发挥着作用,在Android开发上我认为也一样有用。做为一名Android开发者,你也许只会用到Android开发知识中的20%,有80%其实你学了也不一定会用。而面试官也一样,他也可能只掌握了20%的知识,而且一个面...
继续阅读 »

感悟

这个世界有一个“二八原则”在好多地方都发挥着作用,在Android开发上我认为也一样有用。做为一名Android开发者,你也许只会用到Android开发知识中的20%,有80%其实你学了也不一定会用。

而面试官也一样,他也可能只掌握了20%的知识,而且一个面试也不会有足够多的时间给你展示你全部的知识,而往往只会注意开发中最常遇到的20%。

这时候,你对这些问题理解的深度就显得尤为重要。回答了10个问题,而每个问题都只是浅显分析,还没有你将一个问题讲得透彻、全面更能让面试官加分。

当然这并不意味着当你要准备跳槽,要做面试准备的时候,你就只盯着几个自己感兴趣的课题,使劲背,使劲学,而其他的知识点就完全不学了。

想要面试的时候完胜面试官,最简便的,最稳妥的办法就是将一套完整系统的面试题全部刷完,然后再进行自我总结。

我知道有很多人最近都在为跳槽换工作面试做准备,所以在这里,我把我所收集到的面试大纲,分享给大家。

  • 阿里巴巴

  • LRUCache原理
  • 图片加载原理
  • 模块化实现(好处,原因)
  • JVM
  • 视频加密传输
  • 统计启动时长,标准
  • 如何保持应用的稳定性
  • ThreadLocal 原理
  • 谈谈classloader
  • 动态布局
  • 热修复,插件化
  • HashMap源码,SpareArray原理
  • 性能优化,怎么保证应用启动不卡顿
  • 怎么去除重复代码
  • SP是进程同步的吗?有什么方法做到同步
  • 介绍下SurfView
  • HashMap实现原理,ConcurrentHashMap 的实现原理
  • BroadcastReceiver,LocalBroadcastReceiver 区别
  • Bundle 机制
  • Handler 机制
  • android 事件传递机制
  • 线程间 操作 List
  • App启动流程,从点击桌面开始
  • 动态加载
  • 类加载器
  • OSGI
  • Https请求慢的解决办法,DNS,携带数据,直接访问IP
  • GC回收策略
  • 画出 Android 的大体架构图
  • 描述清点击 Android Studio 的 build 按钮后发生了什么
  • 大体说清一个应用程序安装到手机上时发生了什么;
  • 对 Dalvik、ART 虚拟机有基本的了解;
  • Android 上的 Inter-Process-Communication 跨进程通信时如何工作的;
  • App 是如何沙箱化,为什么要这么做;
  • 权限管理系统(底层的权限是如何进行 grant 的)
  • 进程和 Application 的生命周期;
  • 系统启动流程 Zygote进程 –> SystemServer进程 –> 各种系统服务 –> 应用进程
  • recycleview listview 的区别,性能
  • 排序,快速排序的实现
  • 树:B+树的介绍
  • 图:有向无环图的解释
  • TCP/UDP的区别
  • synchronized与Lock的区别
  • volatile
  • Java线程池
  • Java中对象的生命周期
  • 类加载机制
  • 双亲委派模型
  • Android事件分发机制
  • MVP模式
  • RxJava
  • 抽象类和接口的区别
  • 集合 Set实现 Hash 怎么防止碰撞
  • JVM 内存区域 开线程影响哪块内存
  • 垃圾收集机制 对象创建,新生代与老年代
  • 二叉树 深度遍历与广度遍历
  • B树、B+树
  • 消息机制
  • 进程调度
  • 进程与线程
  • 死锁
  • 进程状态
  • JVM内存模型
  • 并发集合了解哪些
  • ConCurrentHashMap实现
  • CAS介绍
  • 开启线程的三种方式,run()和start()方法区别
  • 线程池
  • 常用数据结构简介
  • 判断环(猜测应该是链表环)
  • 排序,堆排序实现
  • 链表反转

  • 腾讯

  • synchronized用法
  • volatile用法
  • 动态权限适配方案,权限组的概念
  • 网络请求缓存处理,okhttp如何处理网络缓存的
  • 图片加载库相关,bitmap如何处理大图,如一张30M的大图,如何预防OOM
  • 进程保活
  • listview图片加载错乱的原理和解决方案
  • https相关,如何验证证书的合法性,https中哪里用了对称加密,哪里用了非对称加密,对加密算法(如RSA)等是否有了解

  • 滴滴

  • MVP
  • 广播(动态注册和静态注册区别,有序广播和标准广播)
  • service生命周期
  • handler实现机制(很多细节需要关注:如线程如何建立和退出消息循环等等)
  • 多线程(关于AsyncTask缺陷引发的思考)
  • 数据库数据迁移问题
  • 设计模式相关(例如Android中哪里使用了观察者模式,单例模式相关)
  • x个苹果,一天只能吃一个、两个、或者三个,问多少天可以吃完
  • TCP与UDP区别与应用(三次握手和四次挥手)涉及到部分细节(如client如何确定自己发送的消息被server收到) HTTP相关 提到过Websocket 问了WebSocket相关以及与socket的区别
  • 是否熟悉Android jni开发,jni如何调用java层代码
  • 进程间通信的方式
  • java注解
  • 计算一个view的嵌套层级
  • 项目组件化的理解
  • 多线程断点续传原理
  • Android系统为什么会设计ContentProvider,进程共享和线程安全问题
  • jvm相关
  • Android相关优化(如内存优化、网络优化、布局优化、电量优化、业务优化)
  • EventBus实现原理

  • 美团

  • static synchronized 方法的多线程访问和作用,同一个类里面两个synchronized方法,两个线程同时访问的问题
  • 内部类和静态内部类和匿名内部类,以及项目中的应用
  • handler发消息给子线程,looper怎么启动
  • View事件传递
  • activity栈
  • 封装view的时候怎么知道view的大小
  • arraylist和linkedlist的区别,以及应用场景
  • 怎么启动service,service和activity怎么进行数据交互
  • 下拉状态栏是不是影响activity的生命周期,如果在onStop的时候做了网络请求,onResume的时候怎么恢复
  • view渲染

  • 今日头条

  • 数据结构中堆的概念,堆排序
  • 死锁的概念,怎么避免死锁
  • ReentrantLock 、synchronized和volatile(n面)
  • HashMap
  • singleTask启动模式
  • 用到的一些开源框架,介绍一个看过源码的,内部实现过程。
  • 消息机制实现
  • ReentrantLock的内部实现
  • App启动崩溃异常捕捉
  • 事件传递机制的介绍
  • ListView的优化
  • 二叉树,给出根节点和目标节点,找出从根节点到目标节点的路径
  • 模式MVP,MVC介绍
  • 断点续传的实现
  • 集合的接口和具体实现类,介绍
  • TreeMap具体实现
  • synchronized与ReentrantLock
  • 手写生产者/消费者模式
  • 逻辑地址与物理地址,为什么使用逻辑地址
  • 一个无序,不重复数组,输出N个元素,使得N个元素的和相加为M,给出时间复杂度、空间复杂度。手写算法
  • Android进程分类
  • 前台切换到后台,然后再回到前台,Activity生命周期回调方法。弹出Dialog,生命值周期回调方法。
  • Activity的启动模式

  • 爱奇艺

  • RxJava的功能与原理实现
  • RecycleView的使用,原理,RecycleView优化
  • ANR的原因
  • 四大组件
  • Service的开启方式
  • Activity与Service通信的方式
  • Activity之间的通信方式
  • HashMap的实现,与HashSet的区别
  • JVM内存模型,内存区域
  • Java中同步使用的关键字,死锁
  • MVP模式
  • Java设计模式,观察者模式
  • Activity与Fragment之间生命周期比较
  • 广播的使用场景

  • 百度

  • Bitmap 使用时候注意什么?
  • Oom 是否可以try catch ?
  • 内存泄露如何产生?
  • 适配器模式,装饰者模式,外观模式的异同?
  • ANR 如何产生?
  • String buffer 与string builder 的区别?
  • 如何保证线程安全?
  • java四中引用
  • Jni 用过么?
  • 多进程场景遇见过么?
  • 关于handler,在任何地方new handler 都是什么线程下
  • sqlite升级,增加字段的语句
  • bitmap recycler 相关
  • 强引用置为null,会不会被回收?
  • glide 使用什么缓存?
  • Glide 内存缓存如何控制大小?
  • 如何保证多线程读写文件的安全?

  • 携程

  • Activity启动模式
  • 广播的使用方式,场景
  • App中唤醒其他进程的实现方式
  • AndroidManifest的作用与理解
  • List,Set,Map的区别
  • HashSet与HashMap怎么判断集合元素重复
  • Java中内存区域与垃圾回收机制
  • EventBus作用,实现方式,代替EventBus的方式
  • Android中开启摄像头的主要步骤

  • 网易

  • 集合
  • concurrenthashmap
  • volatile
  • synchronized与Lock
  • Java线程池
  • wait/notify
  • NIO
  • 垃圾收集器
  • Activity生命周期
  • AlertDialog,popupWindow,Activity区别

  • 小米

  • String 为什么要设计成不可变的?
  • fragment 各种情况下的生命周期
  • Activity 上有 Dialog 的时候按 home 键时的生命周期
  • 横竖屏切换的时候,Activity 各种情况下的生命周期
  • Application 和 Activity 的 context 对象的区别
  • 序列化的作用,以及 Android 两种序列化的区别。
  • List 和 Map 的实现方式以及存储方式。
  • 静态内部类的设计意图。
  • 线程如何关闭,以及如何防止线程的内存泄漏

  • 360

  • 软引用、弱引用区别
  • 垃圾回收
  • 多线程:怎么用、有什么问题要注意;Android线程有没有上限,然后提到线程池的上限
  • JVM
  • OOM,内存泄漏
  • ANR怎么分析解决
  • LinearLayout、RelativeLayout、FrameLayout的特性、使用场景
  • 如何实现Fragment的滑动
  • ViewPager使用细节,如何设置成每次只初始化当前的Fragment,其他的不初始化
  • ListView重用的是什么
  • 进程间通信的机制
  • AIDL机制
  • AsyncTask机制
  • 如何取消AsyncTask
  • 序列化
  • Android为什么引入Parcelable
  • 有没有尝试简化Parcelable的使用
  • AIDL机制
  • 项目:拉活怎么做的
  • 应用安装过程
  • 某海外直播公司
  • 线程和进程的区别?
  • 为什么要有线程,而不是仅仅用进程?
  • 算法判断单链表成环与否?
  • 如何实现线程同步?
  • hashmap数据结构?
  • arraylist 与 linkedlist 异同?
  • object类的equal 和hashcode 方法重写,为什么?
  • hashmap如何put数据(从hashmap源码角度讲解)?
  • 简述IPC?
  • fragment之间传递数据的方式?
  • 简述tcp四次挥手?
  • threadlocal原理
  • 内存泄漏的可能原因?
  • 用IDE如何分析内存泄漏?
  • OOM的可能原因?
  • 线程死锁的4个条件?
  • 差值器&估值器
  • 简述消息机制相关
  • 进程间通信方式?
  • Binder相关?
  • 触摸事件的分发?
  • 简述Activity启动全部过程?
  • okhttp源码?
  • RxJava简介及其源码解读?
  • 性能优化如何分析systrace?
  • 广播的分类?
  • 点击事件被拦截,但是相传到下面的view,如何操作?
  • Glide源码?
  • ActicityThread相关?
  • volatile的原理
  • synchronize的原理
  • lock原理
  • 翻转一个单项链表
  • string to integer
  • 合并多个单有序链表(假设都是递增的)
  • 其他公司
  • 四大组件
  • Android中数据存储方式
  • 微信主页面的实现方式
  • 微信上消息小红点的原理
  • 两个不重复的数组集合中,求共同的元素。
  • 上一问扩展,海量数据,内存中放不下,怎么求出。
  • Java中String的了解。
  • ArrayList与LinkedList区别
  • 堆排序过程,时间复杂度,空间复杂度
  • 快速排序的时间复杂度,空间复杂度
  • RxJava的作用,与平时使用的异步操作来比,优势
  • Android消息机制原理
  • Binder机制介绍
  • 为什么不能在子线程更新UI
  • JVM内存模型
  • Android中进程内存的分配,能不能自己分配定额内存
  • 垃圾回收机制与调用System.gc()区别
  • Android事件分发机制
  • 断点续传的实现
  • RxJava的作用,优缺点

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

收起阅读 »

Flutter踩坑:Android sdkmanager tool not found

今天因为升级了Mac系统,不知道怎么回事flutter开发环境突然报错,最终决定重新安装。正常安装了flutter,然后下载安装了AndroidStudio和VS(平时也会用用VS),然后运行flutter doctor的时候出现了如下错误: Android...
继续阅读 »

今天因为升级了Mac系统,不知道怎么回事flutter开发环境突然报错,最终决定重新安装。正常安装了flutter,然后下载安装了AndroidStudio和VS(平时也会用用VS),然后运行flutter doctor的时候出现了如下错误:



Android sdkmanager tool not found

(/Users/xx/android-sdk/tools/bin/sdkmanager).

Try re-installing or updating your Android SDK,

visit https://flutter.io/setup/#android-setup for detailed instructions.



解决步骤:

看字面意思问题应该是在“/Users/xx/android-sdk/tools/bin/sdkmanager”,但是我尝试了一下发现根本SDK文件夹下根本没有Tools文件夹
百度了一圈,网上给的解决方案,都是将emulator目录下的sdkmanager移动到 tools目录下,可是我根本就没有这个文件夹。
后来在Stack Overflow上找到了原因:Android Studio最新版本中,默认情况下是不会安装Android SDK Tools的,我的版本是3.6。



找到了原因就好解决了:




  • 在窗口左上角andriod studio-偏好设置中找到SDKTools






按图操作就好.png

继续在终端执行

flutter doctor --android-licenses (之后一路选Y就行了)






PS:VScode和AS都要记得装flutter插件,AS还要另外装dart插件


链接:https://www.jianshu.com/p/3237ea28793c 收起阅读 »

UITableViewCell嵌套WKWebView

     今天一直在网上找如何在UITableViewCell嵌套WKWebView,问题还挺多了,最后还是在找到了解决方案,废话不多说,直接看解决方案。正文1. 构建WKWebViewself.webView = [[WKWeb...
继续阅读 »
前言

     今天一直在网上找如何在UITableViewCell嵌套WKWebView,问题还挺多了,最后还是在stackoverflow找到了解决方案,废话不多说,直接看解决方案。

正文

1. 构建WKWebView

self.webView = [[WKWebView alloc] init];
// 创建请求
NSURLRequest *request =[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.jianshu.com"]];
// 加载网页
[self.webView loadRequest:request];

self.webView.scrollView.scrollEnabled = NO;
self.webView.scrollView.bounces = NO;
self.webView.scrollView.showsVerticalScrollIndicator = NO;
self.webView.autoresizingMask = UIViewAutoresizingFlexibleHeight;

// 将webView添加到界面
[self.contentView addSubview:self.webView];

2. cell高度适应WKWebView的内容

cell.webView.navigationDelegate = self;

#pragma mark - WKNavigationDelegate
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {

[webView evaluateJavaScript:@"document.body.offsetHeight" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
// 计算webView高度
self.webViewCellHeight = [result doubleValue];
// 刷新tableView
[self.tableView reloadData];
}];
}

3. 解决加载空白问题
原因:由于WKWebView采用的lazy加载模式,所在的scrollView的滚动被禁用,导致被嵌套的WKCompositingView不进行数据加载。
详细细节请参考:WKWebView刷新机制小探

#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
// 判断webView所在的cell是否可见,如果可见就layout
NSArray *cells = self.tableView.visibleCells;
for (UITableViewCell *cell in cells) {
if ([cell isKindOfClass:[TraitWebViewCell class]]) {
TraitWebViewCell *webCell = (TraitWebViewCell *)cell;

[webCell.webView setNeedsLayout];
}
}

}


链接:https://www.jianshu.com/p/8cdad2282d24
收起阅读 »

Material Design实战之可折叠式标题栏

CollapsingToolbarLayout1.基本介绍CollapsingToolbarLayout是一个作用于Toolbar基础之上的布局,它可以让Toolbar的效果变得更加丰富,不仅仅是展示一个标题栏,还可以实现更加华丽的效果注意:Collapsin...
继续阅读 »

CollapsingToolbarLayout

1.基本介绍

CollapsingToolbarLayout是一个作用于Toolbar基础之上的布局,它可以让Toolbar的效果变得更加丰富,不仅仅是展示一个标题栏,还可以实现更加华丽的效果

注意:
CollapsingToolbarLayout是不能独立存在的,它在设计的时候就被限定只能作为AppBarLayout的直接子布局来使用,而AppBarLayout又必须是CoordinatorLayout的子布局。。

2.具体使用

①我们需要一个具体展示水果详情的页面

在这里就新建一个FruitActivity。

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".FruitActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="250dp"
android:id="@+id/appBar">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/mCollapsingToolbarLayout"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:contentScrim="@color/purple_500"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/fruitImageView"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax"/>
<androidx.appcompat.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:id="@+id/toolbar"
app:layout_collapseMode="pin"/>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>



app:contentScrim用于指定CollapsingToolbarLayout在趋于折叠状态以及折叠之后的背景色。
app:layout_scrollFlags的exitUntilCollapsed表示当CollapsingToolbarLayout随着滚动完成折叠之后就保留在界面上,不再移出屏幕。
里面的ImageView和Toolbar就是标题栏的具体内容。
app:layout_collapseMode表示设置折叠过程中的折叠样式。
然后我们在加一个NestedScrollView,它不仅允许使用滚动来查看屏幕以外的数据,而且还增加了嵌套响应滚动事件的功能。
布局文件全部代码

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".FruitActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="250dp"
android:id="@+id/appBar">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/mCollapsingToolbarLayout"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:contentScrim="@color/purple_500"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/fruitImageView"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax"/>
<androidx.appcompat.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:id="@+id/toolbar"
app:layout_collapseMode="pin"/>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="15dp"
android:layout_marginTop="35dp"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
app:cardCornerRadius="4dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/fruitContentText"
android:layout_margin="10dp"
/>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>



很好理解,就不解释了

②编写功能逻辑

public class FruitActivity extends AppCompatActivity {
static String FRUIT_NAME = "fruit_name";
static String FRUIT_IMAGE_ID = "fruit_image_id";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fruit);
String fruitName = getIntent ().getStringExtra(FRUIT_NAME);
String fruitImageId = getIntent ().getStringExtra(FRUIT_IMAGE_ID);

Toolbar toolbar = findViewById (R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);

CollapsingToolbarLayout collapsingToolbarLayout = findViewById (R.id.mCollapsingToolbarLayout);
collapsingToolbarLayout.setTitle(fruitName);
Glide.with(this).load(fruitImageId).into((ImageView) findViewById (R.id.fruitImageView));

TextView textView = findViewById (R.id.fruitContentText);
}
}


textView.setText("声卡的那句阿奎那飞机咔叽脑筋那就是可能安东尼上级领导那就ask的年纪ask" +
"打卡时间开机卡死的你课件撒的就看撒贷记卡十多年按实际困难贷记卡大卡司你可记得是" +
"多久啊是当年就卡死的你叫阿三的");

toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();

}
});



没啥难度,不说了

③在RecyclerView的适配器中增加点击事件

public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.MyViewHolder> {
List<Fruit> fruits = new ArrayList<>();
Context context;
FruitAdapter(Context context){
this.context = context;
for (int i = 0; i < 30; i++) {
fruits.add(new Fruit("香蕉",R.mipmap.banana));
}
}
@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item,parent,false);
MyViewHolder holder = new MyViewHolder(view);
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = holder.getAdapterPosition();
Fruit fruit = fruits.get(position);
Intent intent = new Intent(context, FruitActivity.class);
intent.putExtra(FruitActivity.FRUIT_NAME,fruit.name);
intent.putExtra(FruitActivity.FRUIT_IMAGE_ID,fruit.imageId);
context.startActivity(intent);
}
});
return holder;
}
}


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

收起阅读 »

前端如何进行用户权限管理

【前端如何进行用户权限管理】1:问题:假如在做一个管理系统,面向老师学生的,学生提交申请,老师负责审核(或者还需要添加其他角色,功能权限都不同)。现在的问题是,每种角色登录看到的界面应该都是不一样的,那这个页面的区分如何实现呢?2:要不要给老师和学生各自设计一...
继续阅读 »

【前端如何进行用户权限管理】

1:问题:
假如在做一个管理系统,面向老师学生的,学生提交申请,老师负责审核(或者还需要添加其他角色,功能权限都不同)。


现在的问题是,每种角色登录看到的界面应该都是不一样的,那这个页面的区分如何实现呢?

2:要不要给老师和学生各自设计一套页面?这样工作量是不是太大了,并且如果还要加入其它角色的话,难道每个角色对应一套代码?

所以我们需要用一套页面适应各种用户角色,并根据身份赋予他们不同权限

3:权限设计与管理是一个很复杂的问题,涉及的东西很多,相比前端,更偏向于后端,在搜集相关资料的过程中,发现掺杂了许多数据库之类的知识,以及几个用于权限管理的java框架,比如spring,比如shiro等等,都属于后端的工作

4:那我们前端能做什么呢?

权限的设计中比较常见的就是RBAC基于角色的访问控制,基本思想是,对系统操作的各种权限不是直接授予具体的用户,而是在用户集合与权限集合之间建立一个角色集合。每一种角色对应一组相应的权限。

一旦用户被分配了适当的角色后,该用户就拥有此角色的所有操作权限。这样做的好处是,不必在每次创建用户时都进行分配权限的操作,只要分配用户相应的角色即可,而且角色的权限变更比用户的权限变更要少得多,这样将简化用户的权限管理,减少系统的开销。

在Angular构建的单页面应用中,要实现这样的架构我们需要额外多做一些事.从整体项目上来讲,大约有3处地方,前端工程师需要进行处理.

1. UI处理(根据用户拥有的权限,判断页面上的一些内容是否显示)

2. 路由处理(当用户访问一个它没有权限访问的url时,跳转到一个错误提示的页面)

3. HTTP请求处理(当我们发送一个数据请求,如果返回的status是401或者401,则通常重定向到一个错误提示的页面)

如何实现?
首先需要在Angular启动之前就获取到当前用户的所有的permissions,然后比较优雅的方式是通过一个service存放这个映射关系.对于UI处理一个页面上的内容是否根据权限进行显示,我们应该通过一个directive来实现.当处理完这些,我们还需要在添加一个路由时额外为其添加一个"permission"属性,并为其赋值表明拥有哪些权限的角色可以跳转这个URL,然后通过Angular监听routeChangeStart事件来进行当前用户是否拥有此URL访问权限的校验.最后还需要一个HTTP拦截器监控当一个请求返回的status是401或者403时,跳转页面到一个错误提示页面.

大致上的工作就是这些,看起来有些多,其实一个个来还是挺好处理的.

在Angular运行之前获取到permission的映射关系



Angular项目通过ng-app启动,但是一些情况下我们是希望Angular项目的启动在我们的控制之中.比如现在这种情况下,我就希望能获取到当前登录用户的所有permission映射关系后,再启动Angular的App.幸运的是Angular本身提供了这种方式,也就是angular.bootstrap().看的仔细的人可能会注意到,这里使用的是$.get(),没有错用的是jQuery而不是Angular的$resource或者$http,因为在这个时候Angular还没有启动,它的function我们还无法使用.

进一步使用上面的代码可以将获取到的映射关系放入一个service作为全局变量来使用.


在取得当前用户的权限集合后,我们将这个集合存档到对应的一个service中,然后又做了2件事:

(1) 将permissions存放到factory变量中,使之一直处于内存中,实现全局变量的作用,但却没有污染命名空间.

(2) 通过$broadcast广播事件,当权限发生变更的时候.

如何确定UI组件的依据权限进行显隐




这里我们需要自己编写一个directive,它会依据权限关系来进行显示或者隐藏元素.

这里看到了比较理想的情况是通关一个has-permission属性校验permission的name,如果当前用户有则显示,没有则隐藏.




扩展一下之前的factory:




路由上的依权限访问
这一部分的实现的思路是这样: 当我们定义一个路由的时候增加一个permission的属性,属性的值就是有哪些权限才能访问当前url.然后通过routeChangeStart事件一直监听url变化.每次变化url的时候,去校验当前要跳转的url是否符合条件,然后决定是跳转成功还是跳转到错误的提示页面.

router.js:






mainController.js 或者 indexController.js (总之是父层Controller)





这里依然用到了之前写的hasPermission,这些东西都是高度可复用的.这样就搞定了,在每次view的route跳转前,在父容器的Controller中判断一些它到底有没有跳转的权限即可.



HTTP请求处理
这个应该相对来说好处理一点,思想的思路也很简单.因为Angular应用推荐的是RESTful风格的接口,所以对于HTTP协议的使用很清晰.对于请求返回的status code如果是401或者403则表示没有权限,就跳转到对应的错误提示页面即可.





当然我们不可能每个请求都去手动校验转发一次,所以肯定需要一个总的filter.代码如下:

写到这里我们就基本实现了在这种前后端分离模式下,前端部分的权限管理和控制。

原文链接:https://blog.csdn.net/jnshu_it/article/details/77511588


收起阅读 »

Android图片轮播-banner

使用步骤以下提供的是最简单的步骤,需要复杂的样式自己可以自定义Step 1.依赖bannerGradledependencies{ compile 'com.youth.banner:banner:2.1.0' }Step 2.添加权限到你的 An...
继续阅读 »

使用步骤

以下提供的是最简单的步骤,需要复杂的样式自己可以自定义

Step 1.依赖banner

Gradle

dependencies{
compile 'com.youth.banner:banner:2.1.0'
}

Step 2.添加权限到你的 AndroidManifest.xml


<uses-permission android:name="android.permission.INTERNET" />

Step 3.在布局文件中添加Banner,可以设置自定义属性

!!!此步骤可以省略,可以直接在Activity或者Fragment中new Banner();

<com.youth.banner.Banner
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/banner"
android:layout_width="match_parent"
android:layout_height="高度自己设置" />

Step 4.继承BannerAdapter,和RecyclerView的Adapter一样(如果你只是图片轮播也可以使用默认的)

!!!此步骤可以省略,图片轮播提供有默认适配器,其他的没有提供是因为大家的可变性要求不确定,所以直接自定义的比较好。

/**
* 自定义布局,下面是常见的图片样式,更多实现可以看demo,可以自己随意发挥
*/
public class ImageAdapter extends BannerAdapter<DataBean, ImageAdapter.BannerViewHolder> {

public ImageAdapter(List<DataBean> mDatas) {
//设置数据,也可以调用banner提供的方法,或者自己在adapter中实现
super(mDatas);
}

//创建ViewHolder,可以用viewType这个字段来区分不同的ViewHolder
@Override
public BannerViewHolder onCreateHolder(ViewGroup parent, int viewType) {
ImageView imageView = new ImageView(parent.getContext());
//注意,必须设置为match_parent,这个是viewpager2强制要求的
imageView.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
return new BannerViewHolder(imageView);
}

@Override
public void onBindView(BannerViewHolder holder, DataBean data, int position, int size) {
holder.imageView.setImageResource(data.imageRes);
}

class BannerViewHolder extends RecyclerView.ViewHolder {
ImageView imageView;

public BannerViewHolder(@NonNull ImageView view) {
super(view);
this.imageView = view;
}
}
}

Step 5.Banner具体方法调用

public class BannerActivity extends AppCompatActivity {
public void useBanner() {
//--------------------------简单使用-------------------------------
banner.addBannerLifecycleObserver(this)//添加生命周期观察者
.setAdapter(new BannerExampleAdapter(DataBean.getTestData()))
.setIndicator(new CircleIndicator(this));

//—————————————————————————如果你想偷懒,而又只是图片轮播————————————————————————
banner.setAdapter(new BannerImageAdapter<DataBean>(DataBean.getTestData3()) {
@Override
public void onBindView(BannerImageHolder holder, DataBean data, int position, int size) {
//图片加载自己实现
Glide.with(holder.itemView)
.load(data.imageUrl)
.apply(RequestOptions.bitmapTransform(new RoundedCorners(30)))
.into(holder.imageView);
}
})
.addBannerLifecycleObserver(this)//添加生命周期观察者
.setIndicator(new CircleIndicator(this));
//更多使用方法仔细阅读文档,或者查看demo
}
}

Banner使用中优化体验

如果你需要考虑更好的体验,可以看看下面的代码

Step 1.(可选)生命周期改变时

public class BannerActivity {

//方法一:自己控制banner的生命周期

@Override
protected void onStart() {
super.onStart();
//开始轮播
banner.start();
}

@Override
protected void onStop() {
super.onStop();
//停止轮播
banner.stop();
}

@Override
protected void onDestroy() {
super.onDestroy();
//销毁
banner.destroy();
}

//方法二:调用banner的addBannerLifecycleObserver()方法,让banner自己控制

protected void onCreate(Bundle savedInstanceState) {
//添加生命周期观察者
banner.addBannerLifecycleObserver(this);
}
}

常见问题(收录被反复询问的问题)

  • 网络图片加载不出来?

    banner本身不提供图片加载功能,首先确认banner本身使用是否正确,具体参考demo, 然后请检查你的图片加载框架或者网络请求框架,服务端也可能加了https安全认证,是看下是否报有证书相关错误

  • 怎么实现视频轮播?

    demo中有实现类似淘宝商品详情的效果,第一个放视频,后面的放的是图片,并且可以设置首尾不能滑动。 因为大家使用的播放器不一样业务环境也不同,具体情况自己把握,demo就是给一个思路哈!可以参考和修改

  • 我想指定轮播开始的位置?

    现在提供了setStartPosition()方法,在sheAdapter和setDatas直接调用一次就行了,当然setAdapter后通过setCurrentItem设置也行

  • 父控件滑动时,banner切换会获取焦点,然后自动全部显示。不想让banner获取焦点可以给父控件加上:

        //banner也一定要用最新版哦!
    android:focusable="true"
    android:focusableInTouchMode="true"


代码下载:banner-master.zip

原文链接:https://github.com/SenhLinsh/Android-Hot-Libraries

收起阅读 »

彻底解决小程序无法触发SESSION问题

一、首先找到第一次发起网络请求的地址,将服务器返回set-cookie当全局变量存储起来wx.request({ ...... success: function(res) { console.log(res.header); //set-co...
继续阅读 »

一、首先找到第一次发起网络请求的地址,将服务器返回set-cookie当全局变量存储起来

wx.request({
......
success: function(res) {
console.log(res.header);
//set-cookie:PHPSESSID=ic4vj84aaavqgb800k82etisu0; path=/; domain=.fengkui.net

// 登录成功,获取第一次的sessionid,存储起来
// 注意:Set-Cookie(开发者工具中调试全部小写)(远程调试和线上首字母大写)
wx.setStorageSync("sessionid", res.header["Set-Cookie"]);
}
})

二、请求时带上将sessionid放入request的header头中传到服务器,服务器端可直接在cookie中获取

wx.request({
......
header: {
'content-type': 'application/json', // 默认值
'cookie': wx.getStorageSync("sessionid")
//读取sessionid,当作cookie传入后台将PHPSESSID做session_id使用
},
success: function(res) {
console.log(res)
}
})

三、后台获取cookie中的PHPSESSID,将PHPSESSID当作session_id使用

<?php
// 判断$_COOKIE['PHPSESSID']是否存在,存在则作session_id使用
if ($_COOKIE['PHPSESSID']) {
session_id($_COOKIE['PHPSESSID']);
}

session_start();
echo session_id();


原文链接:https://blog.csdn.net/qq_41654694/article/details/85991846

收起阅读 »

iOS 一个OC对象在内存中的布局&&占用多少内存

一.先来看看我们平时接触的NSObject NSObject *objc = [[NSObject alloc]init]的本质 在内存中,这行代码就把objc转在底层实现中转成了一个结构体,其底层C++编译成结构体为: struct NSObject_I...
继续阅读 »

一.先来看看我们平时接触的NSObject



  • NSObject *objc = [[NSObject alloc]init]的本质

    在内存中,这行代码就把objc转在底层实现中转成了一个结构体,其底层C++编译成结构体为:


struct NSObject_IMPL {
Class isa;
};

在64位机中,一个isa占8个字节,在32位机中,一个isa占4个字节(当然苹果后面的机型都是64位的,这里我们着重讲解64位机)

  • 我们先来看看这个创建好的objc占多少个字节


int main(int argc, char * argv[]) {

@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
//定义一个objc
NSObject *objc = [[NSObject alloc]init];
//打印内存
NSLog(@"tu-%zd",class_getInstanceSize([NSObject class]));
NSLog(@"tu-%zd",malloc_size((__bridge const void *)(objc)));
}

}




其打印结果为:



objc打印结果





  • 为什么一个是8一个是16
    • 我们先来认识一下class_getInstanceSize、malloc_size的区别

      1.class_getInstanceSize:是一个函数(调用时需要开辟额外的内存空间),程序运行时才获取,计算的是类的大小(至少需要的大小)即实例对象的大小->结构体内存对齐

      2.创建的对象【至少】需要的内存大小不考虑malloc函数的话,内存对齐一般是以【8】对齐

      3.#import <objc/runtime.h>使用这个函数时倒入runtime运行时



    • malloc_size:堆空间【实际】分配给对象的内存大小 -系统内存对齐



      1. 在Mac、iOS中的malloc函数分配的内存大小总是【16】的倍数 即指针指向的内存大小

      2. import <malloc/malloc.h>使用时倒入这个框架





  • sizeof:是一个运算符,获取的是类型的大小(int、size_t、结构体、指针变量等),这些数值在程序编译时就转成常数,程序运行时是直接获取的
  • 看到上面对两个函数的认识,应该知道为什么输出的一个是8,一个是16了吧,当内存申请<16时,在底层分配的时候,系统会默认最低16个字节,系统给objc16个字节,而objc用到的是8个字节(没添加任何成员变量之前)

二.内存对齐



  • 在上面的基础上我们新建一个类Student继承NSObject,那么对于student的底层C++编译实现就变成了:


struct Student {
struct NSObject_IMPL NSOBJECT_IVARS;
};


也就是说,继承关系,子类直接将父类的isa引用进来




  • 对于class_getInstanceSize(也就是类本质的内存对其)

    1.在student中创建成员变量:
@interface Student : NSObject
{
@public
int _age;
int _no;
int _tc;
}
@end

其底层C++编译结构体就变成了


struct Student {
struct NSObject_IMPL NSOBJECT_IVARS;
int _age;
int _no;
int _tc;
};



  • 打印结果:


 //定义一个objc
Student *objc = [[Student alloc]init];
//打印内存
NSLog(@"tu-%zd",class_getInstanceSize([Student class]));
NSLog(@"tu-%zd",malloc_size((__bridge const void *)(objc)));

2020-09-08 12:35:27.158568+0800 OC底层[1549:79836] tu-24

2020-09-08 12:35:27.159046+0800 OC底层[1549:79836] tu-32




  • 先来说说24的由来





由于创建对象的时候,内存是以8对齐,上面我们讲到一个对象里面包含了一个isa占8个字节,对于student来说它有四个成员变量,isa,age,no,tc,共占8+4+4+4=20字节,但是由于内存以8对齐的原因,我们看到的输出是24,

所以class_getInstanceSize在计算实例大小的时候就是24,其白色区域表示空出了四个字节

再来看看32的由来
上面我们说到malloc_size指的是实际堆分配的空间,它以16字节对齐
可以看到,空白的区域为空出了12个字节,总共为32个字节

三.添加属性


  • 添加属性


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

}
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSArray *array;
@end

其在底层C++编译就变成了


struct Student {
struct NSObject_IMPL NSOBJECT_IVARS;
int _age;
int _no;
int _tc;
NSString _name;
NSArray _array;
};


默认的会将属性生成的_name添加进结构体中,计算相应的大小



总结:所以在实际计算类的占用空间大小的时候,根据添加的成员变量就可以计算出一个实例占用的内存大小(即计算出结构体的大小24,然后告诉系统,系统调用calloc分配内存的时候按照16对齐原则分配)

收起阅读 »

vue 重复点击菜单,路由重复报错

报错信息vue-router在3.0版本以上时,重复点菜单,控制台会报错,虽然不影响使用,但是最好处理下这个问题,不然也可能会影响调试其他问题。报错原因vue-router在3.0版本以上时 ,回调形式改成了promise api,返回的是promise,如果...
继续阅读 »

报错信息

vue-router在3.0版本以上时,重复点菜单,控制台会报错,虽然不影响使用,但是最好处理下这个问题,不然也可能会影响调试其他问题。


报错原因
vue-router在3.0版本以上时 ,回调形式改成了promise api,返回的是promise,如果没有捕获到错误,控制台始终会出现如上图的报错
node_module/vue-router/dist/vue-router.js 搜VueRouter.prototype.push

解决方法

1.降低vue-router的版本

npm i vue-router@3.0 -S

2.在vue.use(Router)使用路由插件之前插入如下代码

//获取原型对象上的push函数
const originalPush = Router.prototype.push
//修改原型对象中的push方法
Router.prototype.push = function push (location) {
return originalPush.call(this, location).catch(err => err)
}

3.捕获异常

// 捕获router.push异常
this.$router.push(route).catch(err => {
console.log('输出报错',err)

4.补齐router第三个参数

// 补齐router.push()的第三个参数
this.$router.push(route, () => {}, (e) => {
console.log('输出报错',e)
})

本文链接:https://blog.csdn.net/pinbolei/article/details/115620529


收起阅读 »

深入理解vue中的slot与slot-scope

写在前面vue中关于插槽的文档说明很短,语言又写的很凝练,再加上其和methods,data,computed等常用选项使用频率、使用先后上的差别,这就有可能造成初次接触插槽的开发者容易产生“算了吧,回头再学,反正已经可以写基础组件了”,于是就关闭了vue说明...
继续阅读 »

写在前面

vue中关于插槽的文档说明很短,语言又写的很凝练,再加上其和methods,data,computed等常用选项使用频率、使用先后上的差别,这就有可能造成初次接触插槽的开发者容易产生“算了吧,回头再学,反正已经可以写基础组件了”,于是就关闭了vue说明文档。

实际上,插槽的概念很简单,下面通过分三部分来讲。这个部分也是按照vue说明文档的顺序来写的。

进入三部分之前,先让还没接触过插槽的同学对什么是插槽有一个简单的概念:插槽,也就是slot,是组件的一块HTML模板,这块模板显示不显示、以及怎样显示由父组件来决定。 实际上,一个slot最核心的两个问题这里就点出来了,是显示不显示怎样显示

由于插槽是一块模板,所以,对于任何一个组件,从模板种类的角度来分,其实都可以分为非插槽模板插槽模板两大类。
非插槽模板指的是html模板,指的是‘div、span、ul、table’这些,非插槽模板的显示与隐藏以及怎样显示由插件自身控制;插槽模板是slot,它是一个空壳子,因为它显示与隐藏以及最后用什么样的html模板显示由父组件控制。但是插槽显示的位置确由子组件自身决定,slot写在组件template的哪块,父组件传过来的模板将来就显示在哪块

单个插槽 | 默认插槽 | 匿名插槽

首先是单个插槽,单个插槽是vue的官方叫法,但是其实也可以叫它默认插槽,或者与具名插槽相对,我们可以叫它匿名插槽。因为它不用设置name属性。

单个插槽可以放置在组件的任意位置,但是就像它的名字一样,一个组件中只能有一个该类插槽。相对应的,具名插槽就可以有很多个,只要名字(name属性)不同就可以了。

下面通过一个例子来展示。

父组件:

<template>
<div class="father">
<h3>这里是父组件</h3>
<child>
<div class="tmpl">
<span>菜单1</span>
<span>菜单2</span>
<span>菜单3</span>
<span>菜单4</span>
<span>菜单5</span>
<span>菜单6</span>
</div>
</child>
</div>
</template>

子组件:

<template>
<div class="child">
<h3>这里是子组件</h3>
<slot></slot>
</div>
</template>

在这个例子里,因为父组件在<child></child>里面写了html模板,那么子组件的匿名插槽这块模板就是下面这样。也就是说,子组件的匿名插槽被使用了,是被下面这块模板使用了。

<div class="tmpl">
<span>菜单1</span>
<span>菜单2</span>
<span>菜单3</span>
<span>菜单4</span>
<span>菜单5</span>
<span>菜单6</span>
</div>

最终的渲染结果如图所示:


注:所有demo都加了样式,以方便观察。其中,父组件以灰色背景填充,子组件都以浅蓝色填充。

具名插槽

匿名插槽没有name属性,所以是匿名插槽,那么,插槽加了name属性,就变成了具名插槽。具名插槽可以在一个组件中出现N次。出现在不同的位置。下面的例子,就是一个有两个具名插槽单个插槽的组件,这三个插槽被父组件用同一套css样式显示了出来,不同的是内容上略有区别。

父组件:

<template>
<div class="father">
<h3>这里是父组件</h3>
<child>
<div class="tmpl" slot="up">
<span>菜单1</span>
<span>菜单2</span>
<span>菜单3</span>
<span>菜单4</span>
<span>菜单5</span>
<span>菜单6</span>
</div>
<div class="tmpl" slot="down">
<span>菜单-1</span>
<span>菜单-2</span>
<span>菜单-3</span>
<span>菜单-4</span>
<span>菜单-5</span>
<span>菜单-6</span>
</div>
<div class="tmpl">
<span>菜单->1</span>
<span>菜单->2</span>
<span>菜单->3</span>
<span>菜单->4</span>
<span>菜单->5</span>
<span>菜单->6</span>
</div>
</child>
</div>
</template>

子组件:

<template>
<div class="child">
// 具名插槽
<slot name="up"></slot>
<h3>这里是子组件</h3>
// 具名插槽
<slot name="down"></slot>
// 匿名插槽
<slot></slot>
</div>
</template>

显示结果如图:



可以看到,父组件通过html模板上的slot属性关联具名插槽。没有slot属性的html模板默认关联匿名插槽。

作用域插槽 | 带数据的插槽

最后,就是我们的作用域插槽。这个稍微难理解一点。官方叫它作用域插槽,实际上,对比前面两种插槽,我们可以叫它带数据的插槽。什么意思呢,就是前面两种,都是在组件的template里面写

匿名插槽
<slot></slot>
具名插槽
<slot name="up"></slot>

但是作用域插槽要求,在slot上面绑定数据。也就是你得写成大概下面这个样子。

<slot name="up" :data="data"></slot>
export default {
data: function(){
return {
data: ['zhangsan','lisi','wanwu','zhaoliu','tianqi','xiaoba']
}
},
}

我们前面说了,插槽最后显示不显示是看父组件有没有在child下面写模板,像下面那样。

<child>
html模板
</child>

写了,插槽就总得在浏览器上显示点东西,东西就是html该有的模样,没写,插槽就是空壳子,啥都没有。
OK,我们说有html模板的情况,就是父组件会往子组件插模板的情况,那到底插一套什么样的样式呢,这由父组件的html+css共同决定,但是这套样式里面的内容呢?

正因为作用域插槽绑定了一套数据,父组件可以拿来用。于是,情况就变成了这样:样式父组件说了算,但内容可以显示子组件插槽绑定的。

我们再来对比,作用域插槽和单个插槽和具名插槽的区别,因为单个插槽和具名插槽不绑定数据,所以父组件是提供的模板要既包括样式由包括内容的,上面的例子中,你看到的文字,“菜单1”,“菜单2”都是父组件自己提供的内容;而作用域插槽,父组件只需要提供一套样式(在确实用作用域插槽绑定的数据的前提下)。

下面的例子,你就能看到,父组件提供了三种样式(分别是flex、ul、直接显示),都没有提供数据,数据使用的都是子组件插槽自己绑定的那个人名数组。

父组件:

<template>
<div class="father">
<h3>这里是父组件</h3>
<!--第一次使用:用flex展示数据-->
<child>
<template slot-scope="user">
<div class="tmpl">
<span v-for="item in user.data">{{item}}</span>
</div>
</template>

</child>

<!--第二次使用:用列表展示数据-->
<child>
<template slot-scope="user">
<ul>
<li v-for="item in user.data">{{item}}</li>
</ul>
</template>

</child>

<!--第三次使用:直接显示数据-->
<child>
<template slot-scope="user">
{{user.data}}
</template>

</child>

<!--第四次使用:不使用其提供的数据, 作用域插槽退变成匿名插槽-->
<child>
我就是模板
</child>
</div>
</template>

子组件:

<template>
<div class="child">

<h3>这里是子组件</h3>
// 作用域插槽
<slot :data="data"></slot>
</div>
</template>

export default {
data: function(){
return {
data: ['zhangsan','lisi','wanwu','zhaoliu','tianqi','xiaoba']
}
}
}

结果如图所示:



github

以上三个demo就放在GitHub了,有需要的可以去取。使用非常方便,是基于vue-cli搭建工程。

https://github.com/cunzaizhuyi/vue-slot-demo

转载地址:https://segmentfault.com/a/1190000012996217

收起阅读 »

iOS之解决崩溃Collection <__NSArrayM: 0xb550c30> was mutated while being enumerated.

崩溃提示:Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <CALayerArray: 0x14df0bd0> was ...
继续阅读 »

崩溃提示:Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <CALayerArray: 0x14df0bd0> was mutated while being enumerated.'



当程序出现这个提示的时候,是因为你一边便利数组,又同时修改这个数组里面的内容,导致崩溃,网上的方法如下:

NSMutableArray * arrayTemp = xxx;

NSArray * array = [NSArray arrayWithArray: arrayTemp];

for (NSDictionary * dic in array) {

if (condition){

[arrayTemp removeObject:dic];

}

}

这种方法就是在定义一个一模一样的数组,便利数组A然后操作数组B

今天终于找到了一个更快接的删除数组里面的内容以及修改数组里面的内容的方法:

NSMutableArray *tempArray = [[NSMutableArray alloc]initWithObjects:@"12",@"23",@"34",@"45",@"56", nil];

[tempArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {

if ([obj isEqualToString:@"34"]) {

*stop = YES;

if (*stop == YES) {

[tempArray replaceObjectAtIndex:idx withObject:@"3333333"];

}

}

if (*stop) {

NSLog(@"array is %@",tempArray);

}

}];



利用block来操作,根据查阅资料,发现block便利比for便利快20%左右,这个的原理是这样的:

找到符合的条件之后,暂停遍历,然后修改数组的内容

转自:https://www.cnblogs.com/rglmuselily/p/6249015.html

收起阅读 »

Android Handler消息传递机制

Android中只允许UI线程(也就是主线程)修改Activity里的UI组件。实际开发中,新启动的线程需要周期性地改变界面组件的属性值就需要借助Handler的消息传递机制。Handler类Handler类的主要作用:在新启动的线程中发送消息在主线程中获取、...
继续阅读 »

Android中只允许UI线程(也就是主线程)修改Activity里的UI组件。实际开发中,新启动的线程需要周期性地改变界面组件的属性值就需要借助Handler的消息传递机制。

Handler类

Handler类的主要作用:

在新启动的线程中发送消息
在主线程中获取、处理消息
Handler类包含如下方法用于发送、处理消息。

handleMessage(Message msg):处理消息的方法。该方法通常用于被重写。
hasMessages(int what):检查消息队列中是否包含what属性为指定值的消息。
hasMessages(int what,Object object):检查消息队列中是否包含what属性为指定值且object属性为指定对象的消息。
多个重载的 Message obtainMessage():获取消息。
sendEmptyMessage(int what):发送空消息。
sendEmptyMessageDelayed(int what,long delayMillis):指定多少毫秒之后发送空消
sendMessage(Message msg):立即发送消息。
sendMessageDelayed(Message msg,long delayMillis):指定多少毫秒之后发送消息。
借助于上面这些方法,程序可以方便地利用Handler来进行消息传递。
关于Handler的源码解读,可参考别人写的《Android 多线程之 Handler 源码分析》

实例:自动轮播图片

本实例通过一个新线程来周期性的修改ImageView所显示的图片(因为不允许其他线程访问Activity的界面组件,故在程序中发送消息通知系统更新ImageView组件,故不需要实例Looper),布局文件非常简单,故直接给程序代码:

package com.example.testapp1.activity;

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import com.example.testapp1.R;
import com.example.testapp1.control.RoundImageView;

import java.lang.ref.WeakReference;
import java.util.Timer;
import java.util.TimerTask;

public class NextActivity extends AppCompatActivity {
private RoundImageView imageShow;

static class ImageHandler extends Handler {
private WeakReference nextActivityWeakReference;

public ImageHandler(WeakReference nextActivityWeakReference) {
this.nextActivityWeakReference = nextActivityWeakReference;
}

private int[] imageIds = new int[]{R.drawable.a383f7735d8cd09fb81ff979b2f3d599
, R.drawable.b6ab4abe4db592b27ea678345b0c3416
, R.mipmap.head1
, R.drawable.b6ab4abe4db592b27ea678345b0c3416};
private int currentImageId = 0;

@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
if (msg.what == 0x1233) {
nextActivityWeakReference.get().imageShow.setImageResource(imageIds[currentImageId++ % imageIds.length]);
}
}
}

ImageHandler imageHandler = new ImageHandler(new WeakReference<>(this));

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.next);
imageShow = findViewById(R.id.headImg);
new Timer().schedule(new TimerTask() {
@Override
public void run() {
imageHandler.sendEmptyMessage(0x1233);
}
}, 0, 2000);
}
}




上述代码中,TimeTask对象的本质就是启动一条新线程。

Handler、Loop、MessageQueue的工作原理

Message: Handler接收和处理的消息对象。
Looper:每个线程只能拥有一个Looper。它的loop方法负责读取 MessageQueue中的消息,读到信息之后就把消息交给发送该消息的Handler进行处理。
MessageQueue:消息队列,它采用先进先出的方式来管理Message。程序创建Looper对象时,会在它的构造器中创建MessageQueue对象。Looper的构造器源代码如下:

private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}



该构造器使用了private修饰,表明程序员无法通过构造器创建Looper对象。从上面的代码不难看出,程序在初始化Looper时会创建一个与之关联的 MessagQueue,这个MessageOuee就负责管理消息。

Handler:它的作用有两个,即发送消息和处理消息,程序使用Handler发送消息,由Handler发送的消息必须被送到指定的MessageQueue。也就是说,如果希望Handler正常工作,必须在当前线程中有一个MessageQueue;否则消息就没有 MessageQueue进行保存了。不过MessageQueue是由Looper负责管理的,也就是说,如果希望Handler正常工作,必须在当前线程中有一个Looper对象。为了保证当前线程中有Looper对象,可以分如下两种情况处理。
在主UI线程中,系统已经初始化了一个Looper对象,因此程序直接创建Handler即可,然后就可通过Handler来发送消息、处理消息了。
程序员自己启动的子线程,必须自己创建一个Looper对象,并启动它。创建 Looper对象调用它的prepare(方法即可。
prepare()方法保证每个线程最多只有一个Looper对象。prepare()方法的源代码如下:

public static void prepare() {
prepare(true);
}

private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}



接下来调用Looper的静态loop()方法来启动它。loop()方法使用一个死循环不断取出MessageQueue中的消息,并将取出的消息分给该消息对应的Handler进行处理。下面是Looper类的loop()方法的源代码:

/**
* Run the message queue in this thread. Be sure to call
* {@link #quit()} to end the loop.
*/
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
if (me.mInLoop) {
Slog.w(TAG, "Loop again would have the queued messages be executed"
+ " before this one completed.");
}

me.mInLoop = true;
final MessageQueue queue = me.mQueue;

// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();

// Allow overriding a threshold with a system prop. e.g.
// adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
final int thresholdOverride =
SystemProperties.getInt("log.looper."
+ Process.myUid() + "."
+ Thread.currentThread().getName()
+ ".slow", 0);

boolean slowDeliveryDetected = false;

for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}

// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
// Make sure the observer won't change while processing a transaction.
final Observer observer = sObserver;

final long traceTag = me.mTraceTag;
long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
if (thresholdOverride > 0) {
slowDispatchThresholdMs = thresholdOverride;
slowDeliveryThresholdMs = thresholdOverride;
}
final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);

final boolean needStartTime = logSlowDelivery || logSlowDispatch;
final boolean needEndTime = logSlowDispatch;

if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
}

final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
final long dispatchEnd;
Object token = null;
if (observer != null) {
token = observer.messageDispatchStarting();
}
long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);
try {
msg.target.dispatchMessage(msg);
if (observer != null) {
observer.messageDispatched(token, msg);
}
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} catch (Exception exception) {
if (observer != null) {
observer.dispatchingThrewException(token, msg, exception);
}
throw exception;
} finally {
ThreadLocalWorkSource.restore(origWorkSource);
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
if (logSlowDelivery) {
if (slowDeliveryDetected) {
if ((dispatchStart - msg.when) <= 10) {
Slog.w(TAG, "Drained");
slowDeliveryDetected = false;
}
} else {
if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
msg)) {
// Once we write a slow delivery log, suppress until the queue drains.
slowDeliveryDetected = true;
}
}
}
if (logSlowDispatch) {
showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
}

if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}

// Make sure that during the course of dispatching the
// identity of the thread wasn't corrupted.
final long newIdent = Binder.clearCallingIdentity();
if (ident != newIdent) {
Log.wtf(TAG, "Thread identity changed from 0x"
+ Long.toHexString(ident) + " to 0x"
+ Long.toHexString(newIdent) + " while dispatching to "
+ msg.target.getClass().getName() + " "
+ msg.callback + " what=" + msg.what);
}

msg.recycleUnchecked();
}
}


归纳起来,Looper、MessageQueue、Handler各自的作用如下:

Looper:每个线程只有一个Looper,它负责管理MessageQueue,会不断地从MessageQueag中取出消息,并将消息分给对应的Handler处理。
MessageQueue:由Looper负责管理。它采用先进先出的方式来管理Message。
Handler:它能把消息发送给 Looper管理的MessageQueue,并负责处理 Looper分给它的消息。

在线程中使用Handler的步骤如下:

调用Looper的 prepare()方法为当前线程创建Looper对象,创建Looper对象时,它的构造器会创建与之配套的MessageQueue。
有了Looper之后,创建 Handler子类的实例,重写 handleMessage(方法,该方法负责处理来自其他线程的消息。
调用Looper的loopO方法启动Looper。

实例:使用新线程实现点击图片弹出图片内容

1.布局文件:


android:id="@+id/constraintlayout2"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_gravity="center"
android:layout_height="wrap_content">

android:id="@+id/imageView"
android:layout_width="50dp"
android:layout_height="50dp"
tools:ignore="MissingConstraints"
tools:src="@drawable/ic_launcher_foreground" />

android:id="@+id/textView3"
android:layout_width="100dp"
android:layout_height="50dp"
android:gravity="center"
android:visibility="gone"
app:layout_constraintStart_toEndOf="@+id/imageView"
app:layout_constraintTop_toTopOf="@+id/constraintlayout2"
tools:text="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
tools:visibility="visible" />


布局文件比较简单,就是使用约束布局,在其中放入一个图片控件和文本控件(不展示)。
JAVA代码:

private ImageThread imageThread;

class ImageHandler extends Handler {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
if(msg.what == 0x123){
String imageText = msg.getData().getString("ImageText");
Toast.makeText(mContext, imageText, Toast.LENGTH_LONG).show();
}
}
}

class ImageThread extends Thread {
private Handler mHandler;

@Override
public void run() {
Looper.prepare();
mHandler = new ImageHandler();
Looper.loop();
}
}



上述代码定义了一个线程的子类和Handler的子类,在Android Studio的比较新的版本不能直接使用Handler类实例对象并重新handleMessage(已废弃,旧版本可以),必须通过Handler子类实例对象
在Activity的onCreate()或者Fragment的onCreateView()方法中加入以下代码:启动新线程,监听图片的点击事件,向新线程中的Handler发送消息。

imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Message msg = new Message();
msg.what = 0x123;
Bundle bundle = new Bundle();
bundle.putString("ImageText", imageData.getImageText());
msg.setData(bundle);
imageThread.mHandler.sendMessage(msg);
}
});
imageThread = new ImageThread();
imageThread.start();


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

收起阅读 »

Android三方库glide的使用

glideGlide 是一个图片加载库,跟它同类型的库还有 Picasso、Fresco、Universal-Image-Loader 等。glide库的优点:加载类型多样化:Glide 支持 Gif、WebP 等格式的图片。生命周期的绑定:图片请求与页面生命...
继续阅读 »

glide

Glide 是一个图片加载库,跟它同类型的库还有 Picasso、Fresco、Universal-Image-Loader 等。

glide库的优点:

加载类型多样化:Glide 支持 Gif、WebP 等格式的图片。
生命周期的绑定:图片请求与页面生命周期绑定,避免内存泄漏。
使用简单(链式调用),且提供丰富的 Api 功能 (如: 图片裁剪等功能)。
高效的缓存策略:
支持多种缓存策略 (Memory 和 Disk 图片缓存)。
根据 ImageView 的大小来加载相应大小的图片尺寸。
内存开销小,默认使用 RGB_565 格式 (3.x 版本)。
使用 BitmapPool 进行 Bitmap 的复用。
首先,使用glide需要添加依赖,在当前项目的build.gradle下加入以下代码:

implementation 'com.github.bumptech.glide:glide:4.8.0'
其次,在加载图片时,若需要网络请求或者本地内存的访问,需要在当前项目的AndroidManifest.xml中加入请求权限代码:

//用于网络请求

//它可以监听用户的连接状态并在用户重新连接到网络时重启之前失败的请求

//用于硬盘缓存和读取

glide的使用

Glide.with(MainActivity) .load(R.mipmap.image) .into(imageView);

with()方法可以接收Context、Activity或者Fragment类型的参数。
load()方法中不仅可以传入图片地址,还可以传入图片文件File,resource,图片的byte数组等。
into()参数可以直接写图片控件,如需要给其他控件添加背景图片,则需要:

.into(new SimpleTarget(){
@Override
public void onResourceReady(Bitmap resource, Transition transition) {
Drawable drawable = new BitmapDrawable(resource);
mConstraintLayout.setBackground(drawable);
}
});

加载本地图片:

File file = new File(getExternalCacheDir() + "/image.jpg");
Glide.with(this).load(file).into(imageView);

加载应用资源:

int resource = R.drawable.image;
Glide.with(this).load(resource).into(imageView);

加载二进制流:

byte[] image = getImageBytes();
Glide.with(this).load(image).into(imageView);

加载Uri对象:

Uri imageUri = getImageUri();
Glide.with(this).load(imageUri).into(imageView);

注意with()方法中传入的实例会决定Glide加载图片的生命周期,如果传入的是Activity或者Fragment的实例,那么当这个Activity或Fragment被销毁的时候,图片加载也会停止。如果传入的是ApplicationContext,那么只有当应用程序被杀掉的时候,图片加载才会停止。
取消图片:Glide.with(this).load(url).clear();
————————————————
版权声明:本文为CSDN博主「maisomgan」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_45828419/article/details/115632155

收起阅读 »

Android三方库OKHTTP请求的使用

okhttpOkhttp是网络请求框架。OkHttp主要有Get请求、Post请求等功能。使用前,需要添加依赖,在当前项目的build.gradle下加入以下代码:implementation 'com.squareup.okhttp3:okhttp:3.5....
继续阅读 »

okhttp

Okhttp是网络请求框架。OkHttp主要有Get请求、Post请求等功能。
使用前,需要添加依赖,在当前项目的build.gradle下加入以下代码:

implementation 'com.squareup.okhttp3:okhttp:3.5.0'

Okhttp的Get请求
使用OkHttp进行Get请求只需要完成以下四步:

获取OkHttpClient对象
OkHttpClient okHttpClient = new OkHttpClient();

构造Request对象
Request request = new Request.Builder() .get() .url("https://v0.yiketianqi.com/api?version=v62&appid=12646748&appsecret=SLB1jIr8&city=北京") .build();

将Request封装为Call
Call call = okHttpClient.newCall(request);

根据需要调用同步或者异步请求方法
//同步调用,返回Response,会抛出IO异常
Response response = call.execute();

//异步调用,并设置回调函数

call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Toast.makeText(OkHttpActivity.this, "get failed", Toast.LENGTH_SHORT).show();
}

@Override
public void onResponse(Call call, final Response response) throws IOException {
final String res = response.body().string();
runOnUiThread(new Runnable() {
@Override
public void run() {
textView.setText(res);
}
});
}
});



OkHttp进行Post请求
使用OkHttp进行Post请求和进行Get请求很类似,只需要以下五步:

获取OkHttpClient对象
OkHttpClient okHttpClient = new OkHttpClient();
1
构建FormBody或RequestBody或构架我们自己的RequestBody,传入参数

//OkHttp进行Post请求提交键值对
FormBody formBody = new FormBody.Builder()
.add("username", "admin")
.add("password", "admin")
.build();

//OkHttp进行Post请求提交字符串
RequestBody requestBody = RequestBody.create(MediaType.parse("text/plain;charset=utf-8"), "{username:admin;password:admin}");

//OkHttp进行Post请求上传文件
File file = new File(Environment.getExternalStorageDirectory(), "1.png");
if (!file.exists()){
Toast.makeText(this, "文件不存在", Toast.LENGTH_SHORT).show();
}else{
RequestBody requestBody2 = RequestBody.create(MediaType.parse("application/octet-stream"), file);
}

//OkHttp进行Post请求提交表单
File file = new File(Environment.getExternalStorageDirectory(), "1.png");
if (!file.exists()){
Toast.makeText(this, "文件不存在", Toast.LENGTH_SHORT).show();
return;
}
RequestBody muiltipartBody = new MultipartBody.Builder()
//一定要设置这句
.setType(MultipartBody.FORM)
.addFormDataPart("username", "admin")//
.addFormDataPart("password", "admin")//
.addFormDataPart("myfile", "1.png", RequestBody.create(MediaType.parse("application/octet-stream"), file))
.build();
构建Request,将FormBody作为Post方法的参数传入
final Request request = new Request.Builder()
.url("http://www.jianshu.com/")
.post(formBody)
.build();

将Request封装为Call
Call call = okHttpClient.newCall(request);
1
调用请求,重写回调方法
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Toast.makeText(OkHttpActivity.this, "Post Failed", Toast.LENGTH_SHORT).show();
}

@Override
public void onResponse(Call call, Response response) throws IOException {
final String res = response.body().string();
runOnUiThread(new Runnable() {
@Override
public void run() {
textView.setText(res);
}
});
}
});


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

收起阅读 »

即学即用Android Jetpack - Navigation

前言 即学即用Android Jetpack系列Blog的目的是通过学习Android Jetpack完成一个简单的Demo,本文是即学即用Android Jetpack系列Blog的第一篇。 记得去年第一次参加谷歌开发者大会的时候,就被Navigation的...
继续阅读 »

前言


即学即用Android Jetpack系列Blog的目的是通过学习Android Jetpack完成一个简单的Demo,本文是即学即用Android Jetpack系列Blog的第一篇。


记得去年第一次参加谷歌开发者大会的时候,就被Navigation的图形导航界面给迷住了,一句卧槽就代表了小王的全部心情~


目录


一、简介


1. 定义


Navigation是什么呢?谷歌的介绍视频上说:



Navigation是一个可简化Android导航的库和插件



更确切的来说,Navigation是用来管理Fragment的切换,并且可以通过可视化的方式,看见App的交互流程。这完美的契合了Jake Wharton大神单Activity的建议。


2. 优点



  • 处理Fragment的切换(上文已说过)

  • 默认情况下正确处理Fragment的前进和后退

  • 为过渡和动画提供标准化的资源

  • 实现和处理深层连接

  • 可以绑定ToolbarBottomNavigationViewActionBar


  • SafeArgs(Gradle插件) 数据传递时提供类型安全性


  • ViewModel支持


3. 准备


如果想要进行下面的学习,你需要 3.2 或者更高的Android studio


4. 学习方式


最好的学习方式仍然是通过官方文档,下面是官方的学习地址:

谷歌官方教程:Navigation Codelab

谷歌官方文档:Navigation

官方Demo:Demo地址


二、实战

可能我这么解释还是有点抽象,做一个不是那么恰当的比喻,我们可以将Navigation Graph看作一个地图,NavHostFragment看作一个车,以及把NavController看作车中的方向盘,Navigation Graph中可以看出各个地点(Destination)和通往各个地点的路径,NavHostFragment可以到达地图中的各个目的地,但是决定到什么目的地还是方向盘NavController,虽然它取决于开车人(用户)。



第一步 添加依赖


模块层的build.gradle文件需要添加:

ext.navigationVersion = "2.0.0"
dependencies {
//...
implementation "androidx.navigation:navigation-fragment-ktx:$rootProject.navigationVersion"
implementation "androidx.navigation:navigation-ui-ktx:$rootProject.navigationVersion"
}

如果你要使用SafeArgs插件,还要在项目目录下的build.gradle文件添加:

buildscript {
ext.navigationVersion = "2.0.0"
dependencies {
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"
}
}

以及模块下面的build.gradle文件添加:

apply plugin: 'kotlin-android-extensions'
apply plugin: 'androidx.navigation.safeargs'

第二步 创建navigation导航



  1. 创建基础目录:资源文件res目录下创建navigation目录 -> 右击navigation目录New一个Navigation resource file

  2. 创建一个Destination,如果说navigation是我们的导航工具,Destination是我们的目的地,在此之前,我已经写好了一个WelcomeFragmentLoginFragmentRegisterFragment

除了可视化界面之外,我们仍然有必要看一下里面的内容组成,login_navigation.xml

<navigation
...
android:id="@+id/login_navigation"
app:startDestination="@id/welcome">

<fragment
android:id="@+id/login"
android:name="com.joe.jetpackdemo.ui.fragment.login.LoginFragment"
android:label="LoginFragment"
tools:layout="@layout/fragment_login"
/>

<fragment
android:id="@+id/welcome"
android:name="com.joe.jetpackdemo.ui.fragment.login.WelcomeFragment"
android:label="LoginFragment"
tools:layout="@layout/fragment_welcome">
<action
.../>
<action
.../>
</fragment>

<fragment
android:id="@+id/register"
android:name="com.joe.jetpackdemo.ui.fragment.login.RegisterFragment"
android:label="LoginFragment"
tools:layout="@layout/fragment_register"
>

<argument
.../>
</fragment>
</navigation>



我在这里省略了一些不必要的代码


第三步 建立NavHostFragment


我们创建一个新的LoginActivity,在activity_login.xml文件中:

    ...>

android:id="@+id/my_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="@navigation/login_navigation"
app:defaultNavHost="true"
android:layout_width="match_parent"
android:layout_height="match_parent"/>





第四步 界面跳转、参数传递和动画

WelcomeFragment中,点击登录和注册按钮可以分别跳转到LoginFragmentRegisterFragment中。

这里我使用了两种方式实现:


方式一 利用ID导航

目标:WelcomeFragment携带keyname的数据跳转到LoginFragmentLoginFragment接收后显示。

Have a account ? Login按钮的点击事件如下:

btnLogin.setOnClickListener {
// 设置动画参数
val navOption = navOptions {
anim {
enter = R.anim.slide_in_right
exit = R.anim.slide_out_left
popEnter = R.anim.slide_in_left
popExit = R.anim.slide_out_right
}
}
// 参数设置
val bundle = Bundle()
bundle.putString("name","TeaOf")
findNavController().navigate(R.id.login, bundle,navOption)
}

后续LoginFragment的接收代码比较简单,直接获取Fragment中的Bundle即可,这里不再出示代码。

方式二 利用Safe Args

目标:WelcomeFragment通过Safe Args将数据传到RegisterFragmentRegisterFragment接收后显示。

再看一下已经展示过的login_navigation.xml

<navigation
...
android:id="@+id/login_navigation"
app:startDestination="@id/welcome">

<fragment
android:id="@+id/login"
android:name="com.joe.jetpackdemo.ui.fragment.login.LoginFragment"
android:label="LoginFragment"
tools:layout="@layout/fragment_login"
/>

<fragment
android:id="@+id/welcome"
android:name="com.joe.jetpackdemo.ui.fragment.login.WelcomeFragment"
android:label="LoginFragment"
tools:layout="@layout/fragment_welcome">
<action
.../>
<action
.../>
</fragment>

<fragment
android:id="@+id/register"
android:name="com.joe.jetpackdemo.ui.fragment.login.RegisterFragment"
android:label="LoginFragment"
tools:layout="@layout/fragment_register"
>

<argument
.../>
</fragment>
</navigation>



细心的同学可能已经观察到navigation目录下的login_navigation.xml资源文件中的action标签和argument标签,这里需要解释一下:
点击Android studio中的Make Project按钮,可以发现系统为我们生成了两个类



WelcomeFragment中的JOIN US按钮点击事件:
btnRegister.setOnClickListener {
val action = WelcomeFragmentDirections
.actionWelcomeToRegister()
.setEMAIL("TeaOf1995@Gamil.com")
findNavController().navigate(action)
}

RegisterFragment中的接收:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

// ...
val safeArgs:RegisterFragmentArgs by navArgs()
val email = safeArgs.email
mEmailEt.setText(email)
}
需要提及的是,如果不用Safe Argsaction可以由Navigation.createNavigateOnClickListener(R.id.next_action, null)方式生成,感兴趣的同学可以自行编写。


三、更多


Navigation可以绑定menusdrawersbottom navigation,这里我们以bottom navigation为例,我先在navigation目录下新创建了main_navigation.xml,接着新建了MainActivity,下面则是activity_main.xml:

<LinearLayout
...>

<fragment
android:id="@+id/my_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
app:navGraph="@navigation/main_navigation"
app:defaultNavHost="true"
android:layout_height="0dp"
android:layout_weight="1"/>

<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/navigation_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
app:itemIconTint="@color/colorAccent"
app:itemTextColor="@color/colorPrimary"
app:menu="@menu/menu_main"/>

</LinearLayout>



MainActivity中的处理也十分简单:

class MainActivity : AppCompatActivity() {

lateinit var bottomNavigationView: BottomNavigationView

override fun onCreate(savedInstanceState: Bundle?) {
//...
val host: NavHostFragment = supportFragmentManager.findFragmentById(R.id.my_nav_host_fragment) as NavHostFragment
val navController = host.navController
initWidget()
initBottomNavigationView(bottomNavigationView,navController)
}

private fun initBottomNavigationView(bottomNavigationView: BottomNavigationView, navController: NavController) {
bottomNavigationView.setupWithNavController(navController)
}

private fun initWidget() {
bottomNavigationView = findViewById(R.id.navigation_view)
}
}


作者:九心_
链接:https://www.jianshu.com/p/66b93df4b7a6
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

检测项目中是否包含UIWebView

苹果最近废弃了UIWebView的使用,所以要把工程中引用UIWebView的地方全换掉,不然每次提交审核都会发警告邮件,如下: ITMS-90809: Deprecated API Usage - App updates that use UIWebView...
继续阅读 »

苹果最近废弃了UIWebView的使用,所以要把工程中引用UIWebView的地方全换掉,不然每次提交审核都会发警告邮件,如下:

ITMS-90809: Deprecated API Usage - App updates that use UIWebView will no longer be accepted as of December 2020\. Instead, use WKWebView for improved security and reliability. Learn more https://developer.apple.com/documentation/uikit/uiwebview

After you’ve corrected the issues, you can upload a new binary to App Store Connect.


用到UIWebView的场景如下(包括字符串):


1.自己代码中使用了UIWebView控件。
2.第三方库中使用:
1). README.md等资源文件中使用。
这些文件是没有引入项目的,要在pod库里找到相应的库文件夹,然后 show in finder便能找到。
2). 第三方库的注释里有使用UIWebView字眼。
3). 第三方framewok、.a文件等包含UIWebView,都是二进制文件(Binary file),这种情况只能等第三方库更新SDK了。
3.工程的一些本地配置里包含了UIWebView
搜索结果: UserInterfaceState.xcuserstate matches
1.UserInterfaceState.xcuserstate是什么?
该文件为xcode默认自带文件,是xcode的配置信息,git会用这个文件记录下来。 比如:手动删除此文件,退出xcode后重启xcode,此文件会自动创建并跟踪, git push的时候一般忽略此文件



解决:
场景1:
直接搜索替换成WKWebView即可
场景2:
注释和README文件里使用的UIWebView字眼应该是没影响的。至于frameword和.a文件中包含的引用只能等第三方库更新了,例如Twitter的SDK。
场景3:
xcode的配置信息文件,对于打出来的包应该页没啥影响。为了保险起见,还是删掉此文件,然后让Xcode重新生成一个新的。


检测项目中是否包含UIWebView
1.打开终端,cd + 把项目的工程文件所在文件夹拖入终端(即 得到项目的工程文件所在的路径)
2.输入以下命令(注意最后有个点号,而且点号和 UIWebView 之间必须有一个空格):
grep -r UIWebView .
3.以上操作都正确的话,会马上出现工程中带有 UIWebView 的文件的列表(包括在工程中无法搜索到的 .a 文件中对UIWebView 的引用),如下:



替换TwitterKit
在pod文件中,把 pod 'TwitterKit' 替换为 pod 'TwitterKit5'
进入TwitterSDK的github地址 https://github.com/twitter-archive/twitter-kit-ios/issues/120,可以看到如下信息:



链接:https://www.jianshu.com/p/9c1507509896
收起阅读 »

iOS之切换UITabBar再次加载网络数据

我们在开发中,常常遇到这样的问题,点击某一个TabBar后,本TabBar上的控制器页面数据不刷新,原因是因为在App启动之后,第一次点击本TabBar后页面已经走了viewDidLoad,所以除了重新启动不会再次走viewDidLoad,如果把请求方法写在v...
继续阅读 »

我们在开发中,常常遇到这样的问题,点击某一个TabBar后,本TabBar上的控制器页面数据不刷新,原因是因为在App启动之后,第一次点击本TabBar后页面已经走了viewDidLoad,所以除了重新启动不会再次走viewDidLoad,如果把请求方法写在viewDidLoad中,当然不会再次触发啦,但是,苹果早考虑到这个问题,不用咱们写通知事件什么的,废话有点多了,看代码详解:

首先需要在本控制器签订TabBar的协议

UITabBarControllerDelegate

一定要看清楚协议,如果警报 Assigning to 'id<UITabBarControllerDelegate> _Nullable' from incompatible type 'RCFollowOrderViewController *const __strong'那么就证明你的协议签成了UITabBarDelegate

在viewDidLoad 请求一次 

[self requestdata];

协议方法:


//点击的时候触发的方法


-(void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController{


    if (self.tabBarController.selectedIndex==1) {


        [self requestdata];


    }


}



//防止同一个页面一直点击tabbar 的方法


-(BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController{


    UIViewController *tbselect=tabBarController.selectedViewController;


    if([tbselect isEqual:viewController]){


        returnNO;


    }


    returnYES;


}


如果想要点击TabBar一次,就刷新一次界面,就不写防止重复点击的代理方法,试下效果,搞定!


转自:https://www.jianshu.com/p/f52013ef1eea
收起阅读 »

iOS app唤起微信进行分享时出现“未验证应用”

昨天领导反馈app微信分享到朋友圈出现“未验证应用”的提示信息。通过追踪找到了解决办法。问题的原因由于苹果iOS 13系统版本安全升级,为此openSDK在1.8.6版本进行了适配。 1.8.6版本支持Universal Links方式跳转,对openSDK分...
继续阅读 »

昨天领导反馈app微信分享到朋友圈出现“未验证应用”的提示信息。通过追踪找到了解决办法。

问题的原因

由于苹果iOS 13系统版本安全升级,为此openSDK在1.8.6版本进行了适配。 1.8.6版本支持Universal Links方式跳转,对openSDK分享进行合法性校验。

PS:现在openSDK出了最新的版本1.8.7,新增了自检函数checkUniversalLinkReady:,可以帮助开发者排查SDK接入过程中遇到的问题,在哪一步出错了(共7步,见接入文档https://developers.weixin.qq.com/doc/oplatform/Mobile_App/Access_Guide/iOS.html#jump4)。

问题的解决办法

1.配置Universal Links

1)常见并编辑一个名为apple-app-site-association,无需后缀名,务必符合标准的json格式,格式如下:

{

  "applinks": {

"apps":[],

"details": [

  {

"appID": "你的app的teamID + Bundle Identifier",

"paths": ["*"]

  }

]

  }

}

2)将apple-app-site-association文件发给服务器端的同事,让他上传到域名的根目录下或者.well-known的子目录下(这里的域名必须要是可访问的域名,由服务器端的同事给到)。

2.在app里面配置通用链接

1)首先检查一下Xcode-Targets-Signing&Capabilities 是否有Associated Domains,如果没有,需要去开发者账号在identifer里选择跟当前Xcode所用bundle identifier相同的那一组,进去之后,将Associated Domains前面的方框打上勾,如果已经打勾了,配置如下:




2)实现AppDelegate里支持通用链接的实现方法

- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray> * _Nullable))restorationHandler {

  return [WXApi handleOpenUniversalLink:userActivity delegate:self];

}

3)修改微信注册方法

由[WXApi registerApp:kAppid]改为[WXApi registerApp:kAppid universalLink:kUniversalLinks],这里的universal links为第一步第2)里服务器同事给的链接地址

4)配置info.plist

这里如果是从旧版更新WechatOpenSDK1.8.6/7版本的话,需要在这个里面调用微信里的这个方法,并且在Xcode中,选择你的工程设置项,选中“TARGETS”一栏,在 “info”标签栏的“LSApplicationQueriesSchemes“添加weixin和weixinULAPI

5) 微信开放平台配置Universal links

需要把服务器同事给的地址填写到app iOS信息Universal Links,同时app的下载地址也一定要填写app在app store的地址,填写好后保存,开放平台需要审核,可能要等一段时间Universal Links才能生效,我就是昨天下午设置好没生效,今天早上来才生效。

3.检查是否配置好的Universal links是否生效

生效的标准结合这两个文档https://docs.qq.com/doc/DZHNvSGJLS3pqbHNl的步骤和https://developers.weixin.qq.com/doc/oplatform/Mobile_App/Access_Guide/iOS.html#jump4提供的自检过程

4.如果以上步骤都已经完成,并且自检正确,再分享出去,就不会再出现“未验证的应用”字样了。

PS:上周五(8-7)按照上述步骤,不会出现“未验证的应用”字样,到了周一(8-11)分享,发现又会出现,本人手机系统iOS13.5.1,微信7.0.14;换了同事的手机iOS13.1.2,微信7.0.14发现她的就正常,这个问题需要持续关注,个人觉得是微信sdk的bug;打包上线后,等到用户用一段时间后,新版本放量上去让整体错误率下降到90%以下才会从未验证应用中移除,问题得到彻底解决。


转自:https://www.jianshu.com/p/8e2f06d8f45a 收起阅读 »

JavaScript 逐点突破系列 -- 变幻莫测的this指向

JavaScript 逐点突破系列 – 变幻莫测的this指向this指向事件调用环境谁触发事件,函数里面的this指向就是谁let button = document.getElemetById('button')button.onclick = funct...
继续阅读 »

JavaScript 逐点突破系列 – 变幻莫测的this指向

this指向

事件调用环境

谁触发事件,函数里面的this指向就是谁

let button = document.getElemetById('button')
button.onclick = function () {
console.log(this) //button对象
}

全局环境
浏览器环境下

console.log(this) // window

node环境下

console.log(this) // module.exports

函数内部

this最终指向的是调用的对象,和声明没有直接关系

var object = {
name: 'object',
getName: function() {
console.log(this)
}
}
var bar = object.getName // 只是函数声明并未调用
object.getName() // object对象
window.object.getName() // object对象
/* 函数被多层对象所包含,如果函数被最外层对象调用,this指向
的也只是它上一级的对象。*/
bar() // window对象

构造函数

构造函数中的this指向的是实例对象

let fn = function(){
this.id = 'xiaoMing'
console.log(this.id)
}
let fn1 = new fn() //this指向fn1对象

new 的内部原理

【1】创建一个空对象 obj;
【2】把 child 的__proto__ 指向构造函数 parent 的原型对象 prototype,此时便建立了 obj 对象的原型链:child ->parent.prototype->Object.prototype->null
【3】在 child 对象的执行环境调用 parent 函数并传递参数。
【4】考察第 3 步的返回值,如果无返回值 或者 返回一个非对象值,则将 child 作为新对象返回;否则会将 result 作为新对象返回。this绑定的是返回的对象。

function fn(){
this.num = 10;
}
fn.num = 20;
fn.prototype.num = 30;
fn.prototype.method = function() {
console.log(this.num);
}
var prototype = fn.prototype
var method = prototype.method
new fn().method() // 10
prototype.method() // 30
method() // undefined

箭头函数

箭头函数本身没有this和arguments,箭头函数继承上下文的this关键字,也就是说指向上一层作用域的this。
注意点

  1. 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象
function foo() {
return () => {
return () => {
return () => {
console.log('id:', this.id);
};
};
};
}

var f = foo.call({id: 1});
var t1 = f.call({id: 2})()(); // id: 1
var t2 = f().call({id: 3})(); // id: 1
var t3 = f()().call({id: 4}); // id: 1

上面代码之中,只有一个this,就是函数foo的this,所以t1、t2、t3都输出同样的结果。因为所有的内层函数都是箭头函数,都没有自己的this,它们的this其实都是最外层foo函数的this。

除了this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments、super、new.target。

由于箭头函数没有自己的this,所以当然也就不能用call()、apply()、bind()这些方法去改变this的指向。

不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
不可以使用yield命令,因此箭头函数不能用作 Generator 函数。


修改this指向

apply、call、bind

用法

func.call(this, arg1, arg2);
func.apply(this, [arg1, arg2])
func.bind(this, arg1, arg2)()

apply、call的区别

call 需要把参数按顺序传递进去,而 apply 则是把参数放在数组里。

bind和其他两种方法的区别

bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。

原文链接:https://blog.csdn.net/weixin_45495667/article/details/108801000




收起阅读 »

iOS 一个标签自动布局的view

最近在做一个关于标签事件统计功能的view ,网上看了一些别人的demo感觉都不合适,于是想着自己造一个轮子探探水。 事件告警统计关注-事件列表.jpg 主要实现图中所示的功能,话不多少搞起! 第一次写大神们多包涵呀 下面这个方法计算传进来的字符...
继续阅读 »

最近在做一个关于标签事件统计功能的view ,网上看了一些别人的demo感觉都不合适,于是想着自己造一个轮子探探水。









事件告警统计关注-事件列表.jpg



主要实现图中所示的功能,话不多少搞起!



第一次写大神们多包涵呀

下面这个方法计算传进来的字符串数组,实现每个字符串长度的计算,并做换行判断,每一行存进统计数组中

//将标签数组根据type以及其他参数进行分组装入数组
- (void)disposeTags:(NSArray *)aryName aryCount:(NSArray *)aryCount{
NSMutableArray *tags = [NSMutableArray new];//纵向数组
NSMutableArray *subTags = [NSMutableArray new];//横向数组

float originX = _tagOriginX;
for (NSString *tagTitle in aryName) {
NSUInteger index = [aryName indexOfObject:tagTitle];

//计算每个tag的宽度
CGSize contentSize = [tagTitle fdd_sizeWithFont:[UIFont systemFontOfSize:14] constrainedToSize:CGSizeMake(self.frame.size.width-_tagOriginX*2, MAXFLOAT)];

NSMutableDictionary *dict = [NSMutableDictionary new];
dict[@"tagTitle"] = tagTitle;//标签标题
dict[@"tagCount"] =aryCount[index];
dict[@"viewWith"] = [NSString stringWithFormat:@"%f",contentSize.width+_tagSpace+30];//标签的宽度

if (index == 0) {
dict[@"originX"] = [NSString stringWithFormat:@"%f",originX];//标签的X坐标
[subTags addObject:dict];
} else {
if (originX + contentSize.width > self.frame.size.width-_tagOriginX*2) {
//当前标签的X坐标+当前标签的长度>屏幕的横向总长度则换行
[tags addObject:subTags];
//换行标签的起点坐标初始化
originX = _tagOriginX;
dict[@"originX"] = [NSString stringWithFormat:@"%f",originX];//标签的X坐标
subTags = [NSMutableArray new];
[subTags addObject:dict];
} else {
//如果没有超过屏幕则继续加在前一个数组里
dict[@"originX"] = [NSString stringWithFormat:@"%f",originX];//标签的X坐标
[subTags addObject:dict];
}
}

if (index +1 == aryName.count) {
//最后一个标签加完将横向数组加到纵向数组中
[tags addObject:subTags];
disposeAry = tags;
}

//标签的X坐标每次都是前一个标签的宽度+标签左右空隙+标签距下个标签的距离
originX += contentSize.width+_tagHorizontalSpace+_tagSpace+30;
}
}



下面这个方法是计算字符串长度的封装方法,只限字符串计算,用的话可以直接搬走

#pragma mark - 扩展方法


@implementation NSString (FDDExtention)

- (CGSize)fdd_sizeWithFont:(UIFont *)font constrainedToSize:(CGSize)size {
CGSize resultSize;
if ([self respondsToSelector:@selector(boundingRectWithSize:options:attributes:context:)]) {
NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:@selector(boundingRectWithSize:options:attributes:context:)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:self];
[invocation setSelector:@selector(boundingRectWithSize:options:attributes:context:)];
NSDictionary *attributes = @{ NSFontAttributeName:font };
NSStringDrawingOptions options = NSStringDrawingUsesLineFragmentOrigin;
NSStringDrawingContext *context;
[invocation setArgument:&size atIndex:2];
[invocation setArgument:&options atIndex:3];
[invocation setArgument:&attributes atIndex:4];
[invocation setArgument:&context atIndex:5];
[invocation invoke];
CGRect rect;
[invocation getReturnValue:&rect];
resultSize = rect.size;
} else {
NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:@selector(sizeWithFont:constrainedToSize:)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:self];
[invocation setSelector:@selector(sizeWithFont:constrainedToSize:)];
[invocation setArgument:&font atIndex:2];
[invocation setArgument:&size atIndex:3];
[invocation invoke];
[invocation getReturnValue:&resultSize];
}
return resultSize;
}

字符串长度计算完成了,下面就可以进行所有字符串排列的高度了。其实这个蛮简单的 ,统计数组有多少个元素就代表标签排布有多少行。


//获取处理后的tagsView的高度根据标签的数组




  • (float)getDisposeTagsViewHeight:(NSArray *)ary {


    float height = 0;

    if (disposeAry.count > 0) {

    height = _tagOriginY+disposeAry.count*(_tagHeight+_tagVerticalSpace);

    }

    return height;

    }




下面这个将标签加载到view上了,并实现赋值。

-(void)setTagAryName:(NSArray *)tagAryName aryCount:(NSArray *)aryCount delegate:(id)delegate{
_tagDelegate=delegate;
[self disposeTags:tagAryName aryCount:aryCount];
UILabel *label=[[UILabel alloc]initWithFrame:CGRectMake(_tagOriginX, 0, 200, 30)];
label.text=@"事件类别统计:";
label.textColor=_titleColor;
label.textAlignment=NSTextAlignmentLeft;
label.adjustsFontSizeToFitWidth=YES;
[self addSubview:label];
//遍历标签数组,将标签显示在界面上,并给每个标签打上tag加以区分
for (NSArray *iTags in disposeAry) {
NSUInteger i = [disposeAry indexOfObject:iTags];

for (NSDictionary *tagDic in iTags) {
NSUInteger j = [iTags indexOfObject:tagDic];

NSString *tagTitle = tagDic[@"tagTitle"];
float originX = [tagDic[@"originX"] floatValue];
float viewWith = [tagDic[@"viewWith"] floatValue];
NSString *count = tagDic[@"tagCount"];
UIView *NCView=[[UIView alloc]initWithFrame:CGRectMake(originX, _tagOriginY+i*(_tagHeight+_tagVerticalSpace), viewWith, _tagHeight)];
[self addSubview:NCView];
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.frame=CGRectMake(0, 0, viewWith-30, _tagHeight);
button.layer.borderColor = _borderColor.CGColor;
button.layer.borderWidth = _borderWidth;
button.layer.masksToBounds = _masksToBounds;
button.layer.cornerRadius = _cornerRadius;
button.titleLabel.font = [UIFont systemFontOfSize:_titleSize];
[button setTitle:tagTitle forState:UIControlStateNormal];
[button setTitleColor:_titleColor forState:UIControlStateNormal];
[button setBackgroundImage:_normalBackgroundImage forState:UIControlStateNormal];
[button setBackgroundImage:_highlightedBackgroundImage forState:UIControlStateHighlighted];
button.tag = i*iTags.count+j;
[button addTarget:self action:@selector(buttonAction:) forControlEvents:UIControlEventTouchUpInside];
UILabel *label=[[UILabel alloc]initWithFrame:CGRectMake(viewWith-30, 0, 30, _tagHeight)];
label.text=[NSString stringWithFormat:@"X%@",count];
label.textAlignment=NSTextAlignmentCenter;
label.adjustsFontSizeToFitWidth=YES;
label.textColor=_titleColor;
[NCView addSubview:button];
[NCView addSubview:label];
}
}
self.countLabel=[[UILabel alloc]initWithFrame:CGRectMake(_tagOriginX, [self getDisposeTagsViewHeight:disposeAry], 100, 30)];
self.countLabel.text=[NSString stringWithFormat:@"总计:%ld次",_times];
self.countLabel.textAlignment=NSTextAlignmentLeft;
self.countLabel.adjustsFontSizeToFitWidth=YES;
self.countLabel.textColor=_titleColor;
[self addSubview:self.countLabel];
if (disposeAry.count > 0) {
float contentSizeHeight = _tagOriginY+disposeAry.count*(_tagHeight+_tagVerticalSpace);
self.contentSize = CGSizeMake(self.frame.size.width,contentSizeHeight);
}

if (self.frame.size.height <= 0) {
self.frame = CGRectMake(CGRectGetMinX([self frame]), CGRectGetMinY([self frame]), CGRectGetWidth([self frame]), [self getDisposeTagsViewHeight:disposeAry]+30);
}


}


说了这么多,如何调用呢? 那么亮点来了 只需要传进来数组就实现图中的功能。

//计算出全部展示的高度,让maxHeight等于计算出的高度即可,初始化不需要设置高度
NSArray *tagAryCount=@[@"10",@"8",@"7",@"9",@"2",@"4",@"5",@"3",@"4"];
NSArray* tagAryName = @[@"黑色玫瑰",@"比尔沃吉特",@"钢铁烈阳",@"德玛西亚",@"祖安",@"巨神峰",@"雷瑟守备祖安",@"诺克萨斯暗影岛",@"弗雷尔卓德"];
ZHLbView * tagsView = [[ZHLbView alloc] initWithFrame:CGRectMake(0, 200, self.view.frame.size.width, 0)];
[tagsView setTagAryName:tagAryName aryCount:tagAryCount delegate:self];
[self.view addSubview:tagsView];


补充,在demo编写的过程中 ,遇到了一些小问题,就是scrollView如果是VC的第一个子视图的话 其总是要有一个64高度的空白区。
转自:https://www.jianshu.com/p/860af31a03aa 收起阅读 »

多条件筛选菜单-DropDownMenu

示例图:简介一个实用的多条件筛选菜单,在很多App上都能看到这个效果,如美团,爱奇艺电影票等我的博客 自己造轮子--android常用多条件帅选菜单实现思路(类似美团,爱奇艺电影票下拉菜单)特色支持多级菜单你可以完全自定义你的菜单样式,我这里只是封装...
继续阅读 »

示例图:



简介

一个实用的多条件筛选菜单,在很多App上都能看到这个效果,如美团,爱奇艺电影票等

我的博客 自己造轮子--android常用多条件帅选菜单实现思路(类似美团,爱奇艺电影票下拉菜单)

特色

  • 支持多级菜单
  • 你可以完全自定义你的菜单样式,我这里只是封装了一些实用的方法,Tab的切换效果,菜单显示隐藏效果等
  • 并非用popupWindow实现,无卡顿

Gradle Dependency

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

dependencies {
compile 'com.github.dongjunkun:DropDownMenu:1.0.4'
}

使用

添加DropDownMenu 到你的布局文件,如下

<com.yyydjk.library.DropDownMenu
android:id="@+id/dropDownMenu"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:ddmenuTextSize="13sp" //tab字体大小
app:ddtextUnselectedColor="@color/drop_down_unselected" //tab未选中颜色
app:ddtextSelectedColor="@color/drop_down_selected" //tab选中颜色
app:dddividerColor="@color/gray" //分割线颜色
app:ddunderlineColor="@color/gray" //下划线颜色
app:ddmenuSelectedIcon="@mipmap/drop_down_selected_icon" //tab选中状态图标
app:ddmenuUnselectedIcon="@mipmap/drop_down_unselected_icon"//tab未选中状态图标
app:ddmaskColor="@color/mask_color" //遮罩颜色,一般是半透明
app:ddmenuBackgroundColor="@color/white" //tab 背景颜色
app:ddmenuMenuHeightPercent="0.5" 菜单的最大高度,根据屏幕高度的百分比设置
...
/>

我们只需要在java代码中调用下面的代码

 //tabs 所有标题,popupViews  所有菜单,contentView 内容
mDropDownMenu.setDropDownMenu(tabs, popupViews, contentView);

代码下载:DropDownMenu-master.zip

原文链接:https://github.com/dongjunkun/DropDownMenu

收起阅读 »

iOS 实例对象,类对象,元类对象的关联---isa/superclass指针(二)

总的来说,isa,superclass的的关系可以用一副经典的图来表示class的isa指向meta-classclass的superclass指向父类的class作者:枫紫_6174原贴链接:https://www.jianshu.com/p/26f37fb...
继续阅读 »

  • 一.isa指针

  • 上篇文章我们提到了实例对象,类对象和元类对象的存储结构里面都包含了一个isa指针,今天我们来看看它的作用,以及实例对象类对象元类对象之间的关联


  • 实例对象的isa指针


    • 当实例对象(instance)调用对象方法的时候,实例对象的isa指针指向类对象(class),在类对象中,查找对象方法并调用



  • 类对象的isa指针


    • 类对象(class)的isa指针指向元类对象(meta-class),当调用类方法时,类对象的isa指针指向元类对象,并在元类里面找到类方法并调用

      二.类对象的supercl

      • 先两个类,一个Person继承自NSObject,一个类继承自Person
      /// Person继承自NSObject
      @interface Person : NSObject
      -(void)perMethod;
      +(void)perEat;
      @end
      @implementation Person
      -(void)perMethod{
      }
      +(void)perEat{
      }

      @end

      /// student继承自Person
      @interface Student : Person
      -(void)StudentMethod;
      +(void)StudentEat;
      @end
      @implementation Student

      -(void)StudentMethod{
      }
      +(void)StudentEat{
      }



      • 当实例对象调用自身的对象方法时,它在自身的class对象中找到StudentMethod方法
      •         Student *student = [[Student alloc]init];
        [student StudentMethod]



        • 当实例对象调用父类的方法的时候


                Student *student = [[Student alloc]init];
        [student perMethod];当子类调用父类的实例方法的时候,子类的class类对象的superclass指针指向父类,直至基类(NSObject)找到方法并执行(注意,这里指的是实例方法,也就是减号方法)

        三.元类对象的superclass 指针
        当子类调用父类的类方法的时候,子类的superclass指向父类,并查找到相应的类方法,调用

      • [Student perEat];

      • 总的来说,isa,superclass的的关系可以用一副经典的图来表示


      • instance的isa指向class

        class的isa指向meta-class

        meta-class的isa指向基类的meta-class

        class的superclass指向父类的class



      • 作者:枫紫_6174
        原贴链接:https://www.jianshu.com/p/26f37fb21151
收起阅读 »

js实现函数防抖节流

一、什么是函数防抖跟节流?函数防抖: 在事件被触发n秒之后在执行回调函数,如果在n秒内又被触发 ,则重新计时。函数节流: 规定一个单位时间,规定在这个时间内,只能执行一次回调函数,如果在这个时间内呗触发多次,则只有一次失效。表现形式就是它有...
继续阅读 »

一、什么是函数防抖跟节流?

函数防抖: 在事件被触发n秒之后在执行回调函数,如果在n秒内又被触发 ,则重新计时。
函数节流: 规定一个单位时间,规定在这个时间内,只能执行一次回调函数,如果在这个时间内呗触发多次,则只有一次失效。表现形式就是它有自己的一个执行频率。

二、JavaScript实现

1.函数防抖

代码如下(示例):

function debounce(callback, wait) {
let timer = null;
return function(){
let _this = this,
arg = arguments;
if(!!timer){
clearTimeout(timer)
timer = null
}
timer = setTimeout(function(){
callback.call(_this,arg)
},wait)
}
}
let _debounce = debounce(function() {
console.log('dshdihdi')
}, 1000)


2.函数节流

代码如下(示例):

// 函数节流,时间戳版本
function throttle(callback, wait) {
let time = new Date().getTime();
return function() {
let _this = this,
arg = arguments;
let nowTime = new Date().getTime();
if (nowTime - time >= wait){
callback.call(_this,...arg)
time = nowTime
}
}
}
let _throttle = throttle(function(e) {
console.log(e)
console.log(arguments)
}, 1000)


函数节流: 懒加载分页请求资源、音乐播放进度条更新等等

函数防抖: 频繁操作点赞、登录注册、需要提交最新信息等等

原文链接:https://blog.csdn.net/qq_45924621/article/details/115586112

收起阅读 »

iOS 实例对象,类对象,元类对象(一)

OC对象的分类 OC对象主要分为三类:instance(实例对象),class (类对象),meta-class(元类对象) 实例对象: 实例对象就是通过类调用alloc来产生的instance,每一次调用的alloc都是产生新的实例对象,内存地址都是不一...
继续阅读 »

OC对象的分类


OC对象主要分为三类:instance(实例对象),class (类对象),meta-class(元类对象)




  • 实例对象:


    实例对象就是通过类调用alloc来产生的instance,每一次调用的alloc都是产生新的实例对象,内存地址都是不一样的,占据着不同的内存 eg:


        NSObject *objc1 = [[NSObject alloc]init];
NSObject *objc2 = [[NSObject alloc]init];

NSLog(@"instance----%p %p",objc1,objc2);



输出结果:


instance实例对象存储的信息:
1.isa指针

2.其他成员变量
  • 我们平时说打印出来的实例对象的地址开始就是指的是isa的地址,即isa的地址排在最前面,就是我们实例对象的地址


  • 类对象



  • 类对象的获取


        Class Classobjc1 = [objc1 class];
Class Classobjc2 = [objc2 class];
Class Classobjc3 = object_getClass(objc1);
Class Classobjc4 = object_getClass(objc2);
Class Classobjc5 = [NSObject class];
NSLog(@"class---%p %p %p %p %p ",Classobjc1,Classobjc2,Classobjc3,Classobjc4,Classobjc5);

打印结果

2020-09-22 15:48:00.125034+0800 OC底层[1095:69869] class---0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140

从打印的结果我们可以看到,所有指针指向的类对象的地址是一样的,也就是说一个类的类对象只有唯一的一个




  • 类对象的作用




类对象存储的信息:

1.isa指针

2.superclass指针

3.类的方法(method,即减号方法),类的属性(@property),协议信息,成员变量信息(这里的成员变量不是指的值,因为每个对象的值是由每个实例对象所决定的,这里指的是成员变量的类型,比如整形,字典,字符串,以及成员变量的名字)


  • 元类对象




1.元类对象的获取


        Class metaObjc1 = object_getClass([NSObject class]);
Class metaObjc2 = object_getClass(Classobjc1);
Class metaObjc3 = object_getClass(Classobjc3);
Class metaObjc4 = object_getClass(Classobjc5);

打印指针地址

NSLog(@"meta---%p %p %p %p",metaObjc1,metaObjc2,metaObjc3,metaObjc4);
2020-09-22 16:12:10.191008+0800 OC底层[1131:77555] instance----0x60000000c2e0 0x60000000c2f0
2020-09-22 16:12:10.191453+0800 OC底层[1131:77555] class---0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140
2020-09-22 16:12:10.191506+0800 OC底层[1131:77555] meta---0x7fff9381e0f0 0x7fff9381e0f0 0x7fff9381e0f0 0x7fff9381e0f0

获取元类对象的方法就是利用runtime方法,传入类对象,就可以获取该类的元类对象,从打印的结果可以看出,所有的指针地址一样,也就是说一个类的元类只有唯一的一个


特别注意一点:


Class objc = [[NSObject class] class];
Class objcL = [[[NSObject class] class] class];

无论class几次,它返回的始终是类对象

2020-09-22 16:21:11.065008+0800 OC底层[1163:81105] objcClass---0x7fff9381e140--0x7fff9381e140

元类存储结构:

元类的存储结构和类存储结构是一样的,但是存储的信息和用途不一样,元类的存储信息主要包括:

1.isa指针

2.superclass指针

3.类方法(即加号方法)


从图中我们可以看出元类的存储结构和类存储结构一样,只是有一些值为空



  • 判断是否为元类

    class_isMetaClass(objcL);

作者:枫紫_6174
原贴链接:https://www.jianshu.com/p/26f37fb21151
收起阅读 »

iOS Cateogry的深入理解&&initialize方法调用理解(二)

上一篇文章我们讲到了load方法,今天我们来看看initialize新建项目,新建类(和上一篇文章所建的类相同,方便大家理解,具体的类相关关系可以看上一篇文章我的介绍)类结构图如下将原来的load方法换成initialize先告诉大家initialize方法调...
继续阅读 »
  • 上一篇文章我们讲到了load方法,今天我们来看看initialize

新建项目,新建类(和上一篇文章所建的类相同,方便大家理解,具体的类相关关系可以看上一篇文章我的介绍)类结构图如下


将原来的load方法换成initialize




先告诉大家initialize方法调用的时间,以便大家带着答案去理解initialize:在类第一次接收到消息的时候调用,它区别于load(运行时加载类的时候调用),下面我们来深入理解initialize


相信大家在想什么叫第一次接收消息了,我们回到main()


运行程序,输出结果:

说明:NSLog(@"---")是由于我建的是命令行工程,不写这个,貌似不能显示控制台,Xcode版本是12,当然你们的要显示控制台,直接去掉这行代码


从输出结果可以看到没有任何关于initialize的打印,程序直接退出

  • 2.initialize的打印
int main(int argc, const char * argv[]) {
@autoreleasepool {

[TCPerson alloc];
}
return 0;
}

运行结果:


2020-12-04 14:59:17.417072+0800 TCCateogry[1616:79391] TCPerson (TCtest2) +initialize
Program ended with exit code: 0

从上面的输出结果我们可以看到,TCPerson (TCtest2) +initialize打印


load是直接函数指针直接调用,类,分类,继承等等


[TCPerson alloc]就是相当于该类发送消息,但是它只会调用类,分类的其中一个(取决于编译顺序,从输出结果可以看出,initialize走的是objc_msgSend,而load直接通过函数指针直接调用,所以initialize通过isa方法查找调用.


多次向TCPerson发送消息的输出结果

int main(int argc, const char * argv[]) {
@autoreleasepool {

[TCPerson alloc];
[TCPerson alloc];
[TCPerson alloc];
[TCPerson alloc];
}
return 0;
}

输出结果:

2020-12-04 15:11:12.246442+0800 TCCateogry[1659:85317] TCPerson (TCtest2) +initialize
Program ended with exit code: 0

initialize只会调用一次


我们再来看看继承关系中,initialize的调用


int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCStudent alloc];

}
return 0;
}

输出结果:

2020-12-04 15:14:58.705423+0800 TCCateogry[1705:87507] TCPerson (TCtest2) +initialize
2020-12-04 15:14:58.705750+0800 TCCateogry[1705:87507] TCStudent (TCStudentTest2) +initialize
Program ended with exit code: 0

从输出结果来看,子类调用initialize之前,会先调用父类的initialize,再调用自己的initialize,当然无论父类调用initialize,还是子类调用initialize,如果有多个分类(这里指的是父类调用父类的分类,子类调用子类的分类),调用initialize取决于分类的编译顺序(调用后编译分类中的initialize,类似于压栈,先进后出),值得注意的是,无论父类子类的initialize,都只调用一次

int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCPerson alloc];
[TCPerson alloc];
[TCStudent alloc];
[TCStudent alloc];
}
return 0;
}

输出结果:


020-12-04 15:23:27.168243+0800 TCCateogry[1731:91248] TCPerson (TCtest2) +initialize
2020-12-04 15:23:27.168601+0800 TCCateogry[1731:91248] TCStudent (TCStudentTest2) +initialize
Program ended with exit code: 0

如果子类(子类的分类也不实现)不实现initialize,则父类的initialize就调用多次

#import "TCStudent.h"

@implementation TCStudent
//+ (void)initialize{
// NSLog(@"TCStudent +initialize");
//}
@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCPerson alloc];
[TCStudent alloc];
}
return 0;
}


输出结果:


2020-12-04 15:37:09.055459+0800 TCCateogry[1822:98237] TCPerson (TCtest2) +initialize
2020-12-04 15:37:09.055775+0800 TCCateogry[1822:98237] TCPerson (TCtest2) +initialize
Program ended with exit code: 0

如果子类(子类的分类实现initialize)不实现initialize,则子类的initialize不会调用,调用子类分类的initialize(当然多个分类的话,调用哪个的initialize取决于编译顺序)

#import "TCStudent.h"

@implementation TCStudent
+ (void)initialize{
NSLog(@"TCStudent +initialize");
}
@end
#import "TCStudent+TCStudentTest1.h"

@implementation TCStudent (TCStudentTest1)
+ (void)initialize{
NSLog(@"TCStudent (TCStudentTest1) +initialize");
}
@end#import "TCStudent+TCStudentTest2.h"

@implementation TCStudent (TCStudentTest2)
+ (void)initialize{
NSLog(@"TCStudent (TCStudentTest2) +initialize");
}
@end

输出结果:


2020-12-04 15:41:21.863260+0800 TCCateogry[1868:100750] TCPerson (TCtest2) +initialize
2020-12-04 15:41:21.863568+0800 TCCateogry[1868:100750] TCStudent (TCStudentTest2) +initialize
Program ended with exit code: 0


作者:枫紫_6174
原贴链接:https://www.jianshu.com/p/f0150edc0f42
收起阅读 »

JavaScript new 操作符

new 操作符做的事情 - 01创建了一个全新的对象。将对象链接到这个函数的 prototype 对象上。执行构造函数,并将 this 绑定到新创建的对象上。判断构造函数执行返回的结果是否是引用数据类型,若是则返回构造函数执行的结果,否则返回创建的对象。new...
继续阅读 »
new 操作符做的事情 - 01
  1. 创建了一个全新的对象。
  2. 将对象链接到这个函数的 prototype 对象上。
  3. 执行构造函数,并将 this 绑定到新创建的对象上。
  4. 判断构造函数执行返回的结果是否是引用数据类型,若是则返回构造函数执行的结果,否则返回创建的对象。

new 操作符做的事情 - 02

  1. 创建一个全新的对象 (无原型的Object.create(null)) 。
  2. 目的是保存 new 出来的实例的所有属性。
  3. 将构造函数的原型赋值给新创建的对象的原型。
  4. 目的是将构造函数原型上的属性继承下来。
  5. 调用构造函数,并将 this 指向新建的对象。
  6. 目的是让构造函数内的属性全部转交到该对象上,使得 this 指向改变,方法有三 : apply、call、bind 。
  7. 判断构造函数调用的方式,如果是 new 的调用方式,则返回经过加工后的新对象,如果是普通调用方式,则直接返回构造函数调用时的返回值。
function myNew(Constructor, ...args) {
// 判断 Constructor 参数是否是函数
if (typeof Constructor !== 'function') {
return 'Constructor.apply is not a function';
};

// 1、创建了一个全新的对象。
let newObject = {};

// 2、将对象链接到这个函数的 prototype 对象上。
newObject.__proto__ = Constructor.prototype;

// 此处是把 1 / 2 步结合到一起
// const newObject = Object.create(Constructor.prototype);

// 3、执行构造函数,
// 并将 this 绑定到新创建的对象上。
let result = Constructor.apply(newObject, args);

// 4. 判断构造函数执行返回的结果是否是引用数据类型,
// 若是则返回构造函数执行的结果,
// 否则返回创建的对象。
if ((result !== null && typeof result === 'object') || (typeof result === 'function')) {
return result;
} else {
return newObject;
};
};

// 需要被 new 的函数
function NewTest(args) {
this.dataValue = args;
return this;
};

// 定义参数
let dataObj = {
sname: '杨万里',
number: 3,
web: 'vue'
};
let dataArray = [5, 'uniApp', '范仲淹'];

// 执行 myNew 函数
let test = myNew(NewTest, 1, 'JavaScript', '辛弃疾', dataObj, dataArray);
console.log(test); // NewTest {dataValue: Array(5)}


本文链接:https://blog.csdn.net/weixin_51157081/article/details/115577751


收起阅读 »

iOS Cateogry的深入理解&&load方法调用&&分类重写方法的调用顺序(一)

首先先看几个面试问题 类别里面有负载方法么?load方法什么时候调用?load方法有继承么?  1.新建一个项目,并添加TCPerson类,并给TCPerson添加两个分类 2.新建一个TCStudent类继承自TCPerson,并且给TCStuden...
继续阅读 »

首先先看几个面试问题



  • 类别里面有负载方法么?load方法什么时候调用?load方法有继承么?


 1.新建一个项目,并添加TCPerson类,并给TCPerson添加两个分类




2.新建一个TCStudent类继承自TCPerson,并且给TCStudent也添加两个分类


类别里面有负载方法么?


  • 答:分类里面肯定有负载


#import "TCPerson.h"

@implementation TCPerson
(void)load{

}
@end

#import "TCPerson TCtest1.h"

@implementation TCPerson (TCtest1)
(void)load{

}
@end

#import "TCPerson TCTest2.h"

@implementation TCPerson (TCTest2)
(void)load{

}
@end

load方法什么时候调用?




  • load方法在运行时加载类和分类的时候调用load

  • #import 

    int main(int argc, const char * argv[]) {
    @autoreleasepool {

    }
    return 0;
    }


    @implementation TCPerson
    + (void)load{
    NSLog(@"TCPerson +load");
    }
    @end


    @implementation TCPerson (TCtest1)
    + (void)load{
    NSLog(@"TCPerson (TCtest1) +load");
    }
    @end
    @implementation TCPerson (TCTest2)
    + (void)load{
    NSLog(@"TCPerson (TCtest2) +load");
    }
    @end

    可以看到我们在main里面不导入任何的头文件,也不引用任何的类,直接运行,控制台输出结果:


    从输出结果我们可以抛光,三个负载方法都被调用



    如果不是,到底分类的方法是怎么调用的?




    • 首先我们在TCPerson申明一个方法+(void)test和在它的两个分类都重写+(void)test

    #import 

    NS_ASSUME_NONNULL_BEGIN

    @interface TCPerson : NSObject
    + (void)test;
    @end

    NS_ASSUME_NONNULL_END


    #import "TCPerson.h"

    @implementation TCPerson
    + (void)load{
    NSLog(@"TCPerson +load");
    }
    + (void)test{
    NSLog(@"TCPerson +test");
    }
    @end

    分类重写测试

    #import "TCPerson+TCtest1.h"

    @implementation TCPerson (TCtest1)
    + (void)load{
    NSLog(@"TCPerson (TCtest1) +load");
    }
    + (void)test{
    NSLog(@"TCPerson (TCtest1) +test1");
    }
    @end

    #import "TCPerson+TCTest2.h"

    @implementation TCPerson (TCTest2)
    + (void)load{
    NSLog(@"TCPerson (TCtest2) +load");
    }
    + (void)test{
    NSLog(@"TCPerson (TCtest2) +test2");
    }
    @end

    在主要里面我们调用测试

    #import 
    #import "TCPerson.h"
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    [TCPerson test];
    }
    return 0;
    }

    输出结果:

    从输出结果中我们可以看到,只有分类2中的测试被调用,为什么只调用分类2中的测试了?
    因为编译顺序是分类2在后,1在前,这个时候我们改变编译顺序(进行文件就行了)
    其输出结果为:

    细心的老铁会看到,为什么load方法一直都在调用,这是为什么了?它和test方法到底有什么不同了?真的是我们理解中的load不覆盖,test coverage了,所以才出现这种情况么?


    我们打印TCPerson的类方法

    void printMethodNamesOfClass(Class cls)
    {
    unsigned int count;
    // 获得方法数组
    Method *methodList = class_copyMethodList(cls, &count);

    // 存储方法名
    NSMutableString *methodNames = [NSMutableString string];

    // 遍历所有的方法
    for (int i = 0; i < count; i++) {
    // 获得方法
    Method method = methodList[I];
    // 获得方法名
    NSString *methodName = NSStringFromSelector(method_getName(method));
    // 拼接方法名
    [methodNames appendString:methodName];
    [methodNames appendString:@", "];
    }

    // 释放
    free(methodList);

    // 打印方法名
    NSLog(@"%@ %@", cls, methodNames);
    }
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    [TCPerson test];
    printMethodNamesOfClass(object_getClass([TCPerson class]));
    }
    return 0;
    }



    输出结果:

    可以看到,TCPerson的所有类方法名,并非覆盖,三个负载,三个测试,方法都在

    载入原始码分析:查看objc长期原始码我们可以看到:

  • void call_load_methods(void)
    {
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
    // 1. Repeatedly call class +loads until there aren't any more
    while (loadable_classes_used > 0) {
    call_class_loads();
    }

    // 2. Call category +loads ONCE
    more_categories = call_category_loads();

    // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0 || more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
    }

    load方法它是先调用 while (loadable_classes_used > 0) {call_class_loads(); }类的load,再调用more_categories = call_category_loads()分类的load,和编译顺序无关,都会调用

    我们查看call_class_loads()方法

    static void call_class_loads(void)
    {
    int I;

    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;

    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
    Class cls = classes[i].cls;
    load_method_t load_method = (load_method_t)classes[i].method;
    if (!cls) continue;

    if (PrintLoading) {
    _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
    }
    (*load_method)(cls, SEL_load);
    }

    // Destroy the detached list.
    if (classes) free(classes);
    }

    其通过的是load_method_t函数指针直接调用

    函数指针直接调用

    typedef void(*load_method_t)(id, SEL);

    其分类load方法调用也是一样

    static bool call_category_loads(void)
    {
    int i, shift;
    bool new_categories_added = NO;

    // Detach current loadable list.
    struct loadable_category *cats = loadable_categories;
    int used = loadable_categories_used;
    int allocated = loadable_categories_allocated;
    loadable_categories = nil;
    loadable_categories_allocated = 0;
    loadable_categories_used = 0;

    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
    Category cat = cats[i].cat;
    load_method_t load_method = (load_method_t)cats[i].method;
    Class cls;
    if (!cat) continue;

    cls = _category_getClass(cat);
    if (cls && cls->isLoadable()) {
    if (PrintLoading) {
    _objc_inform("LOAD: +[%s(%s) load]\n",
    cls->nameForLogging(),
    _category_getName(cat));
    }
    (*load_method)(cls, SEL_load);
    cats[i].cat = nil;
    }
    }

    为什么test不一样了

    因为test是因为消息机制调用的,objc_msgSend([TCPerson class], @selector(test));消息机制就牵扯到了isa方法查找,test在元类方法里面顺序查找的(关于isa,可以查看我的实例对象,类对象,元类对象的关联---isa/superclass指针(2))里面有详细的关于test的方法调用原理


    load只在加载类的时候调用一次,且先调用类的load,再调用分类的


    load的继承关系调用

    首先我们先看TCStudent


    #import "TCStudent.h"

    @implementation TCStudent

    @end

    不写load方法调用

    TCStudent写上load

  • 从中可以看出子类不写load的方法,调用父类的load,当子类调用load时,先调用父类的load,再调用子类的load,父类子类load取决于你写load方法没有,如果都写了,先调用父类的,再调用子类的


    总结:先调用类的load,如果有子类,则先看子类是否写了load,如果写了,则先调用父类的load,再调用子类的load,当类子类调用完了,再是分类,分类的load取决于编译顺序,先编译,则先调用,test的方法调用走的是消息发送机制,其底层原理和load方法有着本质的区别,消息发送主要取决于isa的方法查找顺序



    作者:枫紫_6174
    原贴链接:https://www.jianshu.com/p/f66921e24ffe
收起阅读 »

iOS UITableView左滑删除功能

一、概述 UITbableView作为列表展示信息,除了展示的功能,有时会用到删除的功能,比如购物车,视频收藏等。删除功能可以直接使用系统自带的删除功能,当横向向左轻扫cell时,右侧出现红色的删除按钮,点击删除当前cell。 二、效果图 效果图.g...
继续阅读 »
一、概述

UITbableView作为列表展示信息,除了展示的功能,有时会用到删除的功能,比如购物车视频收藏等。删除功能可以直接使用系统自带的删除功能,当横向向左轻扫cell时,右侧出现红色的删除按钮,点击删除当前cell。


二、效果图







效果图.gif


三、技术分析


  1. 让tableView进入编辑状态,即tableView.editing = YES

// 取消
[self.tableView setEditing:YES animated:NO];



  1. 返回编辑模式,即实现UITableViewDelegate中的- tableview:editingStyleForRowAtIndexPath:方法,在里面返回删除模式。如果不实现,默认返回的就是删除模式。

-(UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath
{
// 删除
return UITableViewCellEditingStyleDelete;
}



  1. 提交删除操作,即实现UITableViewDelegate中的- tableview:commitEditingStyle:editing StyleForRowAtIndexPath:方法。只要实现此方法,即默认实现了系统横扫出现删除按钮的删除方法。

-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
//只要实现这个方法,就实现了默认滑动删除!!!!!
if (editingStyle == UITableViewCellEditingStyleDelete)
{
// 删除数据
[self _deleteSelectIndexPath:indexPath];
}
}



  1. 如果想把删除按钮改为中文,可以实现-tableView:titleForDeleteConfirmationButtonForRowAtIndexPath:方法。

-(NSString *)tableView:(UITableView *)tableView titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath
{
return @"删除";
}


四、细节处理


  1. 以下属性必须设置为NO,默认为NO,否则会导致删除模式无效,反而成为多选模式

tableView.allowsMultipleSelection = NO;
tableView.allowsSelectionDuringEditing = NO;
tableView.allowsMultipleSelectionDuringEditing = NO;



  1. 侧滑状态下点击编辑按钮的bug。
// 编辑按钮被点击
- (
void)_rightBarButtonItemDidClicked:(UIButton *)sender
{
sender.selected = !sender.isSelected;
if (sender.isSelected) {

// 这个是fix掉:当你左滑删除的时候,再点击右上角编辑按钮, cell上的删除按钮不会消失掉的bug。且必须放在 设置tableView.editing = YES;的前面。
[
self.tableView reloadData];

// 取消
[
self.tableView setEditing:YES animated:NO];
}
else{
// 编辑
[
self.tableView setEditing:NO animated:NO];
}
}

转自:https://www.jianshu.com/p/4c53901062eb 收起阅读 »

OpenGL基础点

一、OpenGL中的坐标系1. 2D笛卡尔坐标系(X轴,Y轴),平面图形,视口(显示窗口区域系数)2. 3D笛卡尔坐标系(X轴,Y轴,Z轴),立体图形3. OpenGL的投影方式有:透视投影:用来渲染立体图形,有远小近大的效果。具有更加逼真的效果正投影:只能用...
继续阅读 »

一、OpenGL中的坐标系

1. 2D笛卡尔坐标系(X轴,Y轴),平面图形,视口(显示窗口区域系数)

2. 3D笛卡尔坐标系(X轴,Y轴,Z轴),立体图形

3. OpenGL的投影方式有:
透视投影:用来渲染立体图形,有远小近大的效果。具有更加逼真的效果
正投影:只能用来设置平面图形

4.坐标系的分类:

① 惯性坐标系:没有什么参考价值,只要用来平移到世界坐标系

② 世界坐标系:大环境中的位置,系统的绝对坐标

③ 物体空间坐标系(局部空间):局部空间中的位置

④ 摄像机坐标系:观察空间(观察者)

5.坐标系之间的变换

① 物体坐标系,模型变换

② 转换到世界坐标系,视变换

③ 转换到观察者坐标系/摄像机坐标系,投影变换

④ 转换到裁剪坐标系,透视除法

⑤ 转换到规范化设备坐标,视口变换

⑥ 转换到屏幕坐标

注:① - ④ 是可以由开发者自定义完成的,⑤ - ⑥是有系统OpenGL来完成的

二、着色器的渲染流程

1.顶点数据

2.顶点着色器:接收顶点数据,单独处理每个顶点

3.细分着色器:
  ① 可选:描述物体形状,在管线中生成新的饿几何平面模型,生成最终形态
  ② 组分控制着色器/细分计算着色器:对所有的图形进行修改几何图元类型或者放弃所有图像

4.几何着色器

5.图元设置

6.剪切:剪切窗口之外的绘制

7.光栅化:输入图元的数学描述,转化为与屏幕对应位置像素片元,简称光栅化

8.片元着色器:片元着色器以及深度值,然后传递到片元测试和混合模块

9.效果

三、补充几个知识点

1.图片的渲染流程:
  ① GPU解码图片
  ② GPU纹理混合、顶点计算、像素点填充计算、渲染到帧缓冲区
  ③ 时钟信号:垂直同步、水平同步
  ④ iOS设备双缓冲机制:显示系统通常会引入两个帧缓冲区,双缓冲机制

2.MVP矩阵:
  ① model:物体的一些变化
  ② view:观察
  ③ projection:3D矩阵

3.强制解压缩:对图片进行重新绘制,得到一张新的压缩后的位图,其中用到的最核心的函数是:GCBitmapContextCreate

收起阅读 »

微信小程序-使用canvas绘制图片,下载,分享

接下来下选择图片// 点击选择图片按钮触发start: function() { let that = this let ctx = wx.createCanvasContext('myCanvas') // 设置canvas背景色, 否则制...
继续阅读 »

需求, 使用canvas绘制图片和文字, 生成图片

<!--pages/canvas/canvas.wxml-->
<view>
<view class="container">
<button bind:tap="start" class="start" size="mini">选择图片</button>
<button bind:tap="downloadCanvas" class="downloadCanvas" size="mini">下载</button>
<button bind:tap="share" class="share" size="mini">分享</button>
<!-- 分享图片功能 -->
</view>
<text>canvas区域</text>
<canvas style='width:{{canvasWidth}}px;height:{{canvasHeight}}px;border: 1px solid grey;' canvas-id='myCanvas'></canvas>
<image v-if="previewImage" :src="{{canvasImage}}"></image>
</view>


图片功能

先设置canvas区域的宽度和高度

js代码

data: {
title: 'canvas绘制图片',
canvasWidth: '', // canvas宽度
canvasHeight: '', // canvas高度
imagePath: '', // 分享的图片路径
leftMargin: 0,
topMargin: 0,
imgInfo: {},
ctx: [],
canvasImage: '',
previewImage: false,
imgProportion: 0.8, // 图片占canvas画布宽度百分比
imgToTop: 100 // 图片到canvas顶部的距离
},
onLoad: function(options) {
var that = this
var sysInfo = wx.getSystemInfo({
success: function(res) {
that.setData({
canvasWidth: res.windowWidth,
// 我这里选择canvas高度是系统高度的80%
canvasHeight: res.windowHeight * 0.8
})
// 根据图片比例, 使图片居中
let leftMargin = (res.windowWidth * (1 - that.data.imgProportion)) / 2
that.setData({
leftMargin
})
}
})
}


接下来下选择图片

// 点击选择图片按钮触发
start: function() {
let that = this
let ctx = wx.createCanvasContext('myCanvas')
// 设置canvas背景色, 否则制作的图片是透明的
ctx.setFillStyle('#f8f8f8')
ctx.fillRect(0, 0, that.data.canvasWidth, that.data.canvasHeight)
this.addImage(ctx)
},
// 添加图片
addImage: function(ctx) {
var that = this;
let imgInfo = that.data.imgInfo
var path
wx.chooseImage({
count: '1',
success(res) {
wx.getImageInfo({
src: res.tempFilePaths[0],
success: function(response) {
// 返回的response里有图片的临时路径和图片信息(高度/宽度)
that.setData({
imgInfo: response,
path: response.path
})
that.drawImage(ctx)
}
})
}
})
this.addTitle(ctx)
},


绘制文字和图

// 绘制图片
drawImage(ctx) {
let that = this
let imgInfo = that.data.imgInfo
let path = that.data.path
// 计算图片宽度 宽度固定 高度等比缩放
let imgWidth = that.data.canvasWidth * that.data.imgProportion
let imgHeight = imgInfo.height / imgInfo.width * imgWidth
// drawImage参数, 下面会说明
ctx.drawImage(path, 0, 0, imgInfo.width, imgInfo.height, that.data.leftMargin, that.data.imgToTop, imgWidth, imgHeight)
ctx.draw()
that.data.previewImage = true
},
//绘制文字
addTitle: function(ctx) {
var str = this.data.title
ctx.font = 'normal bold 16px sans-serif';
ctx.setTextAlign('center'); // 文字居中
ctx.setFillStyle("#222222");
ctx.fillText(str, this.data.canvasWidth / 2, 45) // 文字位置
},


drawImage方法


drawImage 方法允许在 canvas 中插入其他图像( img 和 canvas 元素) 。drawImage函数有三种函数原型:


drawImage(image, dx, dy) 在画布指定位置绘制原图

drawImage(image, dx, dy, dw, dh) 在画布指定位置上按原图大小绘制指定大小的图

drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh) 剪切图像,并在画布上定位被剪切的部分 从 1.9.0 起支持


参数描述

image

所要绘制的图片资源(网络图片要通过 getImageInfo / downloadFile 先下载)

s

需要绘制到画布中的,image的矩形(裁剪)选择框的左上角 x 坐标

sy

需要绘制到画布中的,image的矩形(裁剪)选择框的左上角 y 坐标

sWidth

需要绘制到画布中的,image的矩形(裁剪)选择框的宽度

sHeight

需要绘制到画布中的,image的矩形(裁剪)选择框的高度

dx

image的左上角在目标 canvas 上 x 轴的位置

dy

image的左上角在目标 canvas 上 y 轴的位置

dWidth

在目标画布上绘制imageResource的宽度,允许对绘制的image进行缩放

dHeight

在目标画布上绘制imageResource的高度,允许对绘制的image进行缩放


下载图片

//点击下载按钮保存canvas图片
downloadCanvas: function() {
let that = this;
// 判断用户是否选择了图片
if (that.data.previewImage) {
wx.canvasToTempFilePath({
x: 0,
y: 0,
width: that.canvasWidth,
height: that.canvasWidth,
destWidth: that.canvasWidth,
destHeight: that.canvasHeight,
canvasId: 'myCanvas',
success: function success(res) {
wx.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success(res) {
console.log(res, '保存')
}
})
}
});
} else {
wx.showToast({
title: '请先选择图片',
image: '../../static/img/error.png'
})
}
}


分享图片

// 分享图片
share() {
let that = this
if (that.data.previewImage) {

wx.canvasToTempFilePath({
x: 0,
y: 0,
width: that.canvasWidth,
height: that.canvasWidth,
destWidth: that.canvasWidth,
destHeight: that.canvasHeight,
canvasId: 'myCanvas',
success: function success(res) {
wx.showToast({
icon: 'none',
title: '长按图片分享',
duration: 1500
})
setTimeout(
() => {
wx.previewImage({
urls: [res.tempFilePath]
})
}, 1000)
}
})
} else {
wx.showToast({
title: '请先选择图片',
image: '../../static/img/error.png'
})
}
}



原文链接:https://www.jianshu.com/p/1e54146b8a26

收起阅读 »

H5之外部浏览器唤起微信分享

转自https://blog.csdn.net/qq_18976087/article/details/79095735最近在做一个手机站,要求点击分享可以直接打开微信分享出去。而不是jiathis,share分享这种的点击出来二维码。在网上看了很多,都说AP...
继续阅读 »

转自https://blog.csdn.net/qq_18976087/article/details/79095735

最近在做一个手机站,要求点击分享可以直接打开微信分享出去。而不是jiathis,share分享这种的点击出来二维码。在网上看了很多,都说APP能唤起微信,手机网页实现不了。也找了很多都不能直接唤起微信。

总结出来一个可以直接唤起微信的。适应手机qq浏览器和uc浏览器。

下面上代码,把这些直接放到要转发的页面里就可以了:

html部分:

<script src="mshare.js"></script>//引进mshare.js
<button data-mshare="0">点击弹出原生分享面板</button>
<button data-mshare="1">点击触发朋友圈分享</button>
<button data-mshare="2">点击触发发送给微信朋友</button>


js部分:

<script>
var mshare = new mShare({
title: 'Lorem ipsum dolor sit.',
url: 'http://m.ly.com',
desc: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quaerat inventore minima voluptates.',
img: 'http://placehold.it/150x150'
});
$('button').click(function () {
// 1 ==> 朋友圈 2 ==> 朋友 0 ==> 直接弹出原生
mshare.init(+$(this).data('mshare'));
});
</script>


/**
* 此插件主要作用是在UC和QQ两个主流浏览器
* 上面触发微信分享到朋友圈或发送给朋友的功能
*/
'use strict';
var UA = navigator.appVersion;

/**
* 是否是 UC 浏览器
*/
var uc = UA.split('UCBrowser/').length > 1 ? 1 : 0;

/**
* 判断 qq 浏览器
* 然而qq浏览器分高低版本
* 2 代表高版本
* 1 代表低版本
*/
var qq = UA.split('MQQBrowser/').length > 1 ? 2 : 0;

/**
* 是否是微信
*/
var wx = /micromessenger/i.test(UA);

/**
* 浏览器版本
*/
var qqVs = qq ? parseFloat(UA.split('MQQBrowser/')[1]) : 0;
var ucVs = uc ? parseFloat(UA.split('UCBrowser/')[1]) : 0;

/**
* 获取操作系统信息 iPhone(1) Android(2)
*/
var os = (function () {
var ua = navigator.userAgent;

if (/iphone|ipod/i.test(ua)) {
return 1;
} else if (/android/i.test(ua)) {
return 2;
} else {
return 0;
}
}());

/**
* qq浏览器下面 是否加载好了相应的api文件
*/
var qqBridgeLoaded = false;

// 进一步细化版本和平台判断
if ((qq && qqVs < 5.4 && os == 1) || (qq && qqVs < 5.3 && os == 1)) {
qq = 0;
} else {
if (qq && qqVs < 5.4 && os == 2) {
qq = 1;
} else {
if (uc && ((ucVs < 10.2 && os == 1) || (ucVs < 9.7 && os == 2))) {
uc = 0;
}
}
}
/**
* qq浏览器下面 根据不同版本 加载对应的bridge
* @method loadqqApi
* @param {Function} cb 回调函数
*/
function loadqqApi(cb) {
// qq == 0
if (!qq) {
return cb && cb();
}
var script = document.createElement('script');
script.src = (+qq === 1) ? '//3gimg.qq.com/html5/js/qb.js' : '//jsapi.qq.com/get?api=app.share';
/**
* 需要等加载过 qq 的 bridge 脚本之后
* 再去初始化分享组件
*/
script.onload = function () {
cb && cb();
};
document.body.appendChild(script);
}
/**
* UC浏览器分享
* @method ucShare
*/
function ucShare(config) {
// ['title', 'content', 'url', 'platform', 'disablePlatform', 'source', 'htmlID']
// 关于platform
// ios: kWeixin || kWeixinFriend;
// android: WechatFriends || WechatTimeline
// uc 分享会直接使用截图
var platform = '';
var shareInfo = null;
// 指定了分享类型
if (config.type) {
if (os == 2) {
platform = config.type == 1 ? 'WechatTimeline' : 'WechatFriends';
} else if (os == 1) {
platform = config.type == 1 ? 'kWeixinFriend' : 'kWeixin';
}
}
shareInfo = [config.title, config.desc, config.url, platform, '', '', ''];
// android
if (window.ucweb) {
ucweb.startRequest && ucweb.startRequest('shell.page_share', shareInfo);
return;
}
if (window.ucbrowser) {
ucbrowser.web_share && ucbrowser.web_share.apply(null, shareInfo);
return;
}
}
/**
* qq 浏览器分享函数
* @method qqShare
*/
function qqShare(config) {
var type = config.type;
//微信好友 1, 微信朋友圈 8
type = type ? ((type == 1) ? 8 : 1) : '';
var share = function () {
var shareInfo = {
'url': config.url,
'title': config.title,
'description': config.desc,
'img_url': config.img,
'img_title': config.title,
'to_app': type,
'cus_txt': ''
};
if (window.browser) {
browser.app && browser.app.share(shareInfo);
} else if (window.qb) {
qb.share && qb.share(shareInfo);
}
};
if (qqBridgeLoaded) {
share();
} else {
loadqqApi(share);
}
}
/**
* 对外暴露的接口函数
* @method mShare
* @param {Object} config 配置对象
*/
function mShare(config) {
this.config = config;
this.init = function (type) {
if (typeof type != 'undefined') this.config.type = type;
try {
if (uc) {
ucShare(this.config);
} else if (qq && !wx) {
qqShare(this.config);
}
} catch (e) {}
}
}
// 预加载 qq bridge
loadqqApi(function () {
qqBridgeLoaded = true;
});
if (typeof module === 'object' && module.exports) {
module.exports = mShare;
} else {
window.mShare = mShare;
}
收起阅读 »

iOS打印导入字体名称

通常在开发中,我们的APP中会使用到一些自己定义的字体,关于引用那部分就不细说了,网上百度一堆,下边说一个容易忽略的点。在iOS中,使用字体,不是使用字体包的名称,而是需要导入包体在iOS中对应的名称,打印字体名称如下:for (NSString *fontf...
继续阅读 »

通常在开发中,我们的APP中会使用到一些自己定义的字体,关于引用那部分就不细说了,网上百度一堆,下边说一个容易忽略的点。

在iOS中,使用字体,不是使用字体包的名称,而是需要导入包体在iOS中对应的名称,打印字体名称如下:

for (NSString *fontfamilyname in [UIFont familyNames]) { // font:'Avenir-Heavy
NSLog(@"family:'%@'",fontfamilyname);
for(NSString *fontName in [UIFont fontNamesForFamilyName:fontfamilyname])
{
NSLog(@"\tfont:'%@'",fontName);
}
NSLog(@"-------------");
}

对字体的使用

self.proNameLab.font = [UIFont fontWithName:@"Avenir-Heavy" size:13*ScaleSize];
收起阅读 »

iOS本地数据持久化

在iOS开发中,有很多数据持久化的方案,本文章将介绍以下6种方案: plist文件(序列化) preference(偏好设置) NSKeyedArchiver(归档) SQLite3 FMDB CoreData 沙盒 每个APP的沙盒下面都有相似目录结构,...
继续阅读 »

在iOS开发中,有很多数据持久化的方案,本文章将介绍以下6种方案:



plist文件(序列化)

preference(偏好设置)

NSKeyedArchiver(归档)

SQLite3

FMDB

CoreData



沙盒


每个APP的沙盒下面都有相似目录结构,如图







下面的代码得到的是应用程序目录的路径,在该目录下有三个文件夹:Documents、Library、temp以及一个.app包!该目录下就是应用程序的沙盒,应用程序只能访问该目录下的文件夹!!!



NSString *path = NSHomeDirectory();




1、Documents 目录:您应该将所有的应用程序数据文件写入到这个目录下。这个目录用于存储用户数据。该路径可通过配置实现iTunes共享文件。可被iTunes备份。


2、AppName.app 目录:这是应用程序的程序包目录,包含应用程序的本身。由于应用程序必须经过签名,所以您在运行时不能对这个目录中的内容进行修改,否则可能会使应用程序无法启动。


3、Library 目录:这个目录下有两个子目录:

Preferences 目录:包含应用程序的偏好设置文件。您不应该直接创建偏好设置文件,而是应该使用NSUserDefaults类来取得和设置应用程序的偏好.

Caches 目录:用于存放应用程序专用的支持文件,保存应用程序再次启动过程中需要的信息。

可创建子文件夹。可以用来放置您希望被备份但不希望被用户看到的数据。该路径下的文件夹,除Caches以外,都会被iTunes备份。


4、tmp 目录:这个目录用于存放临时文件,保存应用程序再次启动过程中不需要的信息。该路径下的文件不会被iTunes备份。

// 获取沙盒主目录路径
NSString *homeDir = NSHomeDirectory();
// 获取Documents目录路径
NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
// 获取Library的目录路径
NSString *libDir = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) lastObject];
// 获取Caches目录路径
NSString *cachesDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
// 获取tmp目录路径
NSString *tmpDir = NSTemporaryDirectory();

//获取应用程序程序包中资源文件路径的方法
NSString *imagePath = [[NSBundle mainBundle] pathForResource:@"apple" ofType:@"png"];
UIImage *appleImage = [[UIImage alloc] initWithContentsOfFile:imagePath];


plist文件(序列化)


可以被序列化的类型只有如下几种:

NSArray; //数组
NSMutableArray; //可变数组
NSDictionary; //字典
NSMutableDictionary; //可变字典
NSData; //二进制数据
NSMutableData; //可变二进制数据
NSString; //字符串
NSMutableString; //可变字符串
NSNumber; //基本数据
NSDate; //日期


数据存储与读取的实例:

/**
写入数据到plist
*/
- (void)writeToPlist{
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject;
NSLog(@"写入数据地址%@",path);

NSString *fileName = [path stringByAppendingPathComponent:@"123.plist"];
NSArray *array = @[@"123", @"王佳佳", @"iOS"];
//序列化,把数组存入plist文件
[array writeToFile:fileName atomically:YES];
NSLog(@"写入成功");
}

/**
从plist读取数据

@return 读出数据
*/
- (NSArray *)readFromPlist{
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject;
NSLog(@"读取数据地址%@",path);

NSString *fileName = [path stringByAppendingPathComponent:@"123.plist"];
//反序列化,把plist文件数据读取出来,转为数组
NSArray *result = [NSArray arrayWithContentsOfFile:fileName];
NSLog(@"%@", result);
return result;
}



存储时使用writeToFile:atomically:方法。 其中atomically表示是否需要先写入一个辅助文件,再把辅助文件拷贝到目标文件地址。这是更安全的写入文件方法,一般都写YES。



Preference(偏好设置)


Preference通常用来保存应用程序的配置信息的,一般不要在偏好设置中保存其他数据。


数据存储与读取的实例:

- (void)writeToPreference{

//1.获得NSUserDefaults文件
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
//2.向偏好设置中写入内容
[userDefaults setObject:@"wangjiajia" forKey:@"name"];
[userDefaults setBool:YES forKey:@"sex"];
[userDefaults setInteger:21 forKey:@"age"];
//2.1立即同步
[userDefaults synchronize];

NSString *path = NSHomeDirectory();
}

- (void)readFromPreference{
//获得NSUserDefaults文件
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];

//读取偏好设置
NSString *name = [userDefaults objectForKey:@"name"];
BOOL sex = [userDefaults boolForKey:@"sex"];
NSInteger age = [userDefaults integerForKey:@"age"];
}


使用偏好设置对数据进行保存,它保存的时间是不确定的,会在将来某一时间自动将数据保存到 Preferences 文件夹下,如果需要即刻将数据存储,使用 [defaults synchronize]。



Preference(偏好设置)plist文件(序列化)都是保存在 plist 文件中,但是plist文件(序列化)操作读取时需要把整个plist文件都进行读取,而Preference(偏好设置) 可以直接通过 key-value单个读取。



归档解归档


要使用归档,其归档对象必须实现NSCoding协议



NSCoding协议声明的两个方法都必须实现。

encodeWithCoder:用来说明如何将对象编码到归档中。

initWithCoder:用来说明如何进行解档来获取一个新对象。



数据存储与读取的实例:

/**
归档
*/
- (void)keyedArchiver{
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject;
NSString *file = [path stringByAppendingPathComponent:@"person.data"];
Person *person = [[Person alloc] init];
person.name = @"wangjiajia";
[NSKeyedArchiver archiveRootObject:person toFile:file];
}

/**
解档
*/
- (void)keyedUnarchiver{
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject;
NSString *file = [path stringByAppendingPathComponent:@"person.data"];
Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:file];
if (person) {
NSLog(@"name:%@",person.name);
}
}


SQLite3


以下代码块将会介绍sqlite3.h中主要的API。

/* 打开数据库 */
int sqlite3_open(
const char *filename, /* 数据库路径(UTF-8) */
sqlite3 **pDb /* 返回的数据库句柄 */
);

/* 执行没有返回的SQL语句 */
int sqlite3_exec(
sqlite3 *db, /* 数据库句柄 */
const char *sql, /* SQL语句(UTF-8) */
int (*callback)(void*,int,char**,char**), /* 回调的C函数指针 */
void *arg, /* 回调函数的第一个参数 */
char **errmsg /* 返回的错误信息 */
);

/* 执行有返回结果的SQL语句 */
int sqlite3_prepare_v2(
sqlite3 *db, /* 数据库句柄 */
const char *zSql, /* SQL语句(UTF-8) */
int nByte, /* SQL语句最大长度,-1表示SQL支持的最大长度 */
sqlite3_stmt **ppStmt, /* 返回的查询结果 */
const char **pzTail /* 返回的失败信息*/
);

/* 关闭数据库 */
int sqlite3_close(sqlite3 *db);


处理SQL返回结果的一些API

#pragma mark - 定位记录的方法
/* 在查询结果中定位到一条记录 */
int sqlite3_step(sqlite3_stmt *stmt);
/* 获取当前定位记录的字段名称数目 */
int sqlite3_column_count(sqlite3_stmt *stmt);
/* 获取当前定位记录的第几个字段名称 */
const char * sqlite3_column_name(sqlite3_stmt *stmt, int iCol);
# pragma mark - 获取字段值的方法
/* 获取二进制数据 */
const void * sqlite3_column_blob(sqlite3_stmt *stmt, int iCol);
/* 获取浮点型数据 */
double sqlite3_column_double(sqlite3_stmt *stmt, int iCol);
/* 获取整数数据 */
int sqlite3_column_int(sqlite3_stmt *stmt, int iCol);
/* 获取文本数据 */
const unsigned char * sqlite3_column_text(sqlite3_stmt *stmt, int iCol);


由于其他API相对来说比较简单,这里就只给出执行有返回结果的SQL语句的实例

/* 执行有返回值的SQL语句 */
- (NSArray *)executeQuery:(NSString *)sql{
NSMutableArray *array = [NSMutableArray array];
sqlite3_stmt *stmt; //保存查询结果
//执行SQL语句,返回结果保存在stmt中
int result = sqlite3_prepare_v2(_database, sql.UTF8String, -1, &stmt, NULL);
if (result == SQLITE_OK) {
//每次从stmt中获取一条记录,成功返回SQLITE_ROW,直到全部获取完成,就会返回SQLITE_DONE
while( SQLITE_ROW == sqlite3_step(stmt)) {
//获取一条记录有多少列
int columnCount = sqlite3_column_count(stmt);
//保存一条记录为一个字典
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
for (int i = 0; i < columnCount; i++) {
//获取第i列的字段名称
const char *name = sqlite3_column_name(stmt, i);
//获取第i列的字段值
const unsigned char *value = sqlite3_column_text(stmt, i);
//保存进字典
NSString *nameStr = [NSString stringWithUTF8String:name];
NSString *valueStr = [NSString stringWithUTF8String:(const char *)value];
dict[nameStr] = valueStr;
}
[array addObject:dict];//添加当前记录的字典存储
}
sqlite3_finalize(stmt);//stmt需要手动释放内存
stmt = NULL;
NSLog(@"Query Stmt Success");
return array;
}
NSLog(@"Query Stmt Fail");
return nil;
}


在使用数据库存储时主要存在以下步骤。



1、创建数据库

2、创建数据表

3、数据的“增删改查”操作

4、关闭数据库



在使用sqlite3时,数据表的创建及数据的增删改查都是通过sql语句实现的。下面是一些常用的SQL语句。



创建表:

create table 表名称(字段1,字段2,……,字段n,[表级约束])[TYPE=表类型];

插入记录:

insert into 表名(字段1,……,字段n) values (值1,……,值n);

删除记录:

delete from 表名 where 条件表达式;

修改记录:

update 表名 set 字段名1=值1,……,字段名n=值n where 条件表达式;

查看记录:

select 字段1,……,字段n from 表名 where 条件表达式;



FMDB


FMDB是一种第三方的开源库,FMDB就是对SQLite的API进行了封装,加上了面向对象的思想,让我们不必使用繁琐的C语言API函数,比起直接操作SQLite更加方便。


FMDB主要是使用以下三个类


FMDatabase : 一个单一的SQLite数据库,用于执行SQL语句。

FMResultSet :执行查询一个FMDatabase结果集。

FMDatabaseQueue :在多个线程来执行查询和更新时会使用这个类。



一般的FMDB数据库操作4个:


创建数据库

打开数据库、关闭数据库

执行更新的SQL语句

执行查询的SQL语句



创建数据库

/**
创建数据库
*/
- (void)createDatabase{
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *filePath = [path stringByAppendingPathComponent:@"FMDB.db"];
NSLog(@"数据库路径:%@",filePath);
/**
1. 如果该路径下已经存在该数据库,直接获取该数据库;
2. 如果不存在就创建一个新的数据库;
3. 如果传@"",会在临时目录创建一个空的数据库,当数据库关闭时,数据库文件也被删除;
4. 如果传nil,会在内存中临时创建一个空的数据库,当数据库关闭时,数据库文件也被删除;
*/
self.database = [FMDatabase databaseWithPath:filePath];
}


打开关闭数据库

/* 打开数据库,成功返回YES,失败返回NO */
- (BOOL)open;
/* 关闭数据库,成功返回YES,失败返回NO */
- (BOOL)close;


执行更新的SQL语句

在FMDB里除了查询操作,其他数据库操作都称为更新。而更新操作FMDB给出以下四种方法。

/* 1. 直接使用完整的SQL更新语句 */
[self.database executeUpdate:@"insert into mytable(num,name,sex) values(0,'wangjiajia1','m');"];

NSString *sql = @"insert into mytable(num,name,sex) values(?,?,?);";
/* 2. 使用不完整的SQL更新语句,里面含有待定字符串"?",需要后面的参数进行替代 */
[self.database executeUpdate:sql,@1,@"wangjiajia2",@"m"];

/* 3. 使用不完整的SQL更新语句,里面含有待定字符串"?",需要数组参数里面的参数进行替代 */
[self.database executeUpdate:sql
withArgumentsInArray:@[@2,@"wangjiajia3",@"m"]];

/* 4. SQL语句字符串可以使用字符串格式化,这种我们应该比较熟悉 */
[self.database executeUpdateWithFormat:@"insert into mytable(num,name,sex) values(%d,%@,%@);",4,@"wangjiajia4",@"m"];


执行查询的SQL语句

查询方法与更新方法类似,只不过查询方法存在返回值,可以通过返回值给出的相应方法获取需要的数据。

/* 执行查询SQL语句,返回FMResultSet查询结果 */
- (FMResultSet *)executeQuery:(NSString*)sql, ... ;
- (FMResultSet *)executeQueryWithFormat:(NSString*)format, ... ;
- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments;


执行查询语句实例如下

- (NSArray *)getResultFromDatabase{
//执行查询SQL语句,返回查询结果
FMResultSet *result = [self.database executeQuery:@"select * from mytable"];
NSMutableArray *array = [NSMutableArray array];
//获取查询结果的下一个记录
while ([result next]) {
//根据字段名,获取记录的值,存储到字典中
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
int num = [result intForColumn:@"num"];
NSString *name = [result stringForColumn:@"name"];
NSString *sex = [result stringForColumn:@"sex"];
dict[@"num"] = @(num);
dict[@"name"] = name;
dict[@"sex"] = sex;
//把字典添加进数组中
[array addObject:dict];
}
return array;
}


多线程安全FMDatabaseQueue

由于在多线程同时操作FMDatabase对象时,会造成数据混乱的问题,FMDB提供了一个可以确保线程安全的类(FMDatabaseQueue)。

FMDatabaseQueue的使用比较简单

//创建多线程安全队列对象
self.queue = [FMDatabaseQueue databaseQueueWithPath:filePath];
//在block块内自行相关数据库操作即可
[self.queue inDatabase:^(FMDatabase * _Nonnull db) {
}];


事务

事务,是指作为单个逻辑工作单元执行的一系列操作,要么完整地执行,要么完全地不执行。


比如要更新数据库的大量数据,我们需要确保所有的数据更新成功,才采取这种更新方案,如果在更新期间出现错误,就不能采取这种更新方案了,这就是事务的用处。只有事务提交了,开启事务期间的操作才会生效。

以下是事务的例子

//事务
-(void)transaction {
// 开启事务
[self.database beginTransaction];
BOOL isRollBack = NO;
@try {
for (int i = 0; i<500; i--) {
NSNumber *num = @(i+6);
NSString *name = [[NSString alloc] initWithFormat:@"student_%d",i];
NSString *sex = (i%2==0)?@"f":@"m";
NSString *sql = @"insert into mytable(num,name,sex) values(?,?,?);";
BOOL result = [self.database executeUpdate:sql,num,name,sex];
if ( !result ) {
NSLog(@"插入失败!");
isRollBack = YES;
return;
}
}
}
@catch (NSException *exception) {
isRollBack = YES;
NSLog(@"插入失败,事务回退");
// 事务回退
[self.database rollback];
}
@finally {
if (!isRollBack) {
NSLog(@"插入成功,事务提交");
//事务提交
[self.database commit];
}else{
NSLog(@"插入失败,事务回退");
// 事务回退
[self.database rollback];
}
}
}

//多线程安全事务实例
- (void)transactionByQueue {
//开启事务
[self.queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
BOOL isRollBack = NO;
for (int i = 0; i<500; i++) {
NSNumber *num = @(i+1);
NSString *name = [[NSString alloc] initWithFormat:@"student_%d",i];
NSString *sex = (i%2==0)?@"f":@"m";
NSString *sql = @"insert into mytable(num,name,sex) values(?,?,?);";
BOOL result = [db executeUpdate:sql,num,name,sex];
if ( !result ) {
isRollBack = YES;
return;
}
}

//当最后*rollback的值为YES的时候,事务回退,如果最后*rollback为NO,事务提交
*rollback = isRollBack;
}];
}



CoreData







CoreData核心结构图.png


以下是CoreData常用类的作用描述



PersistentObjectStore:存储持久对象的数据库(例如SQLite,注意CoreData也支持其他类型的数据存储,例如xml、二进制数据等)。

ManagedObjectModel:对象模型,对应Xcode中创建的模型文件。

PersistentStoreCoordinator:对象模型和实体类之间的转换协调器,用于管理不同存储对象的上下文。

ManagedObjectContext:对象管理上下文,负责实体对象和数据库之间的交互。



CoreData主要工作原理如下



读取数据库的数据时,数据库数据先进入数据解析器,根据对应的模板,生成对应的关联对象。

向数据库插入数据时,对象管理器先根据实体描述创建一个空对象,对该对象进行初始化,然后经过数据解析器,根据对应的模板,转化为数据库的数据,插入数据库中。

更新数据库数据时,对象管理器需要先读取数据库的数据,拿到相互关联的对象,对该对象进行修改,修改的数据通过数据解析器,转化为数据库的更新数据,对数据库更新。



CoreData的使用步骤如下



1.添加框架。

2.数据模板和对象模型。

3.创建对象管理上下文。

4.数据的增删改查操作。



在其中第二步的时候要注意,Xcode 8.0 之后和之前的Xcode 版本是有一些区别的。在 8.0之后创建.xcdatamodeld文件之后,添加实体之后会自动生成对应的对应类文件。

收起阅读 »

iOS KVO的原理&&KVO的isa指向

一。简单复习一下KVO的使用 定义一个类,继承自NSObject,并添加一个名称的属性 #import NS_ASSUME_NONNULL_BEGIN @interface TCPerson : NSObject @property (nonato...
继续阅读 »

一。简单复习一下KVO的使用



  • 定义一个类,继承自NSObject,并添加一个名称的属性


#import 

NS_ASSUME_NONNULL_BEGIN

@interface TCPerson : NSObject

@property (nonatomic, copy) NSString *name;

@end

NS_ASSUME_NONNULL_END



  • 在ViewController我们简单的使用一下KVO


#import "ViewController.h"
#import "TCPerson.h"
@interface ViewController ()
@property (nonatomic, strong) TCPerson *person1;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[TCPerson alloc]init];
self.person1.name = @"liu yi fei";
[self.person1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
}

/// 点击屏幕出发改变self.person1的name
/// @param touches touches description
/// @param event event description
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
self.person1.name = @"cang lao shi";
}

/// 监听回调
/// @param keyPath 监听的属性名字
/// @param object 被监听的对象
/// @param change 改变的新/旧值
/// @param context context description
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
NSLog(@"监听到%@对象的%@发生了改变%@",object,keyPath,change);
}

/// 移除观察者
- (void)dealloc{
[self.person1 removeObserver:self forKeyPath:@"name"];
}
@end

当点击屏幕的时候,控制台输出:


2020-09-24 15:53:52.527734+0800 KVO_TC[9255:98204] 监听到对象的name发生了改变{
kind = 1;
new = "cang lao shi";
old = "liu yi fei";
}

二。深入剖析KVO的突破



  • 在-(void)touchesBegan:(NSSet*)触摸事件:(UIEvent *)事件{

    self.person1.name = @“苍老市”;

    }我们知道
    self.person1.name的本质是[self.person1 setName:@“ cang lao shi”];


- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
// self.person1.name = @"cang lao shi";
[self.person1 setName:@"cang lao shi"];
}

在TCPerson的.m文件,我们从写setter方法并打断点,可以看到当我们点击屏幕的时候,我们发现进入了setter方法:


- (void)setName:(NSString *)name{
_name = name;
}


  • 在ViewController我们新建一个person2,代码变成了:


#import "ViewController.h"
#import "TCPerson.h"
@interface ViewController ()
@property (nonatomic, strong) TCPerson *person1;
@property (nonatomic, strong) TCPerson *person2;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[TCPerson alloc]init];
self.person1.name = @"liu yi fei";
[self.person1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];


self.person2 = [[TCPerson alloc] init];
self.person2.name = @"yyyyyyyy";
}

/// 点击屏幕出发改变self.person1的name
/// @param touches touches description
/// @param event event description
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
self.person1.name = @"cang lao shi";
// [self.person1 setName:@"cang lao shi"];

self.person2.name = @"ttttttttt";
}

/// 监听回调
/// @param keyPath 监听的属性名字
/// @param object 被监听的对象
/// @param change 改变的新/旧值
/// @param context context description
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
NSLog(@"监听到%@对象的%@发生了改变%@",object,keyPath,change);
}

/// 移除观察者
- (void)dealloc{
[self.person1 removeObserver:self forKeyPath:@"name"];
}
@end


  • 注意:当我们点击屏幕的时候输出的结果是:


2020-09-24 16:10:36.750153+0800 KVO_TC[9313:105906] 监听到对象的name发生了改变{
kind = 1;
new = "cang lao shi";
old = "liu yi fei";
}




  • 既然我们改变名称的值的时候走的都是setName:setter方法,按理说观察属性变化的时候,person2的值也应该被观察到,为什么它不会观察到person2?




三.KVO的isa指向



  • 上篇文章中我分析了实例对象,类对象,元类对象的isa,既然当我们改变属性值的时候,其本质是调用setter方法,那么在KVO中,person1和person2的setName方法应该存储在类对象中,我们先来看看这两个实例对象的isa指向:

    打开lldb


(lldb) p self.person1.isa
(Class) $0 = NSKVONotifying_TCPerson
Fix-it applied, fixed expression was:
self.person1->isa
(lldb) p self.person2.isa
(Class) $1 = TCPerson
Fix-it applied, fixed expression was:
self.person2->isa
(lldb)


  • 从上面的打印我们看到self.person1的isa指向了NSKVONotifying_TCPerson,而没有添加观察着的self.person2的isa却指向的是TCPerson

  • NSKVONotifying_TCPerson是运行时动态创建的类,继承自TCPerson,其内部实现可以看成(模拟的NSKVONotifying_TCPerson流程,下面的代码不能在xcode中运行):


#import "NSKVONotifying_TCPerson.h"

@implementation NSKVONotifying_TCPerson
//NSKVONotifying_TCPerson的set方法实现,其本质来自于foundation框架
- (void)setName:(NSString *)name{
_NSSetIntVaueAndNotify();
}
//改变过程
void _NSSetIntVaueAndNotify(){
[self willChangeValueForKey:@"name"];
[super setName:name];
[self didChangeValueForKey:@"name"];
}
//通知观察者
- (void)didChangeValueForKey:(NSString *key){
[observe observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context];
}
@end
未添加观察self.person2实例对象的isa指向流程图:

添加观察self.person1实例对象的isa指向流程图:


所以KVO其本质是动态生成一个NSKVONotifying_TCPerson类,继承自TCPerson,当实例对象添加观察着之后,实例对象的isa指向了这个动态创建的类,当其属性发生改变时,调用的是该类的setter方法,而不是父类的类对象中的setter方法

作者:枫紫_6174
原贴链接:https://www.jianshu.com/p/0b6083b91b04
收起阅读 »

iOS Runloop的深入理解

iOS
首先了解一下程序、进程和线程 程序本身只是指令、数据及其组织形式的描述,进程才是程序(那些指令和数据)的真正运行实例。而线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可...
继续阅读 »

首先了解一下程序、进程和线程


程序本身只是指令、数据及其组织形式的描述,进程才是程序(那些指令和数据)的真正运行实例。而线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。


一般来说,一个线程一次只能执行一个任务,执行完成后线程就会退出。所以程序运行的时候,需要一个机制,让线程能随时处理事件但不退出。


以前写游戏的时候就写过这样的东西,通常是一个 do while 循环,让程序一直运转,直到接收到退出信息。


而 RunLoop 就是让线程在没有处理消息时休眠以避免资源占用,在有消息到来时立刻被唤醒。


线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时默认是没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。


在 CoreFoundation 里面关于 RunLoop 有5个类:


CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef


他们的层级关系为,一个 RunLoop 对象包含若干个 Mode 对象,每个 Mode 又包含若干个 Source/Timer/Observer,RunLoop 在运作的时候,一次只能运作与一个 Mode 之下,如果需要切换 Mode,需要退出 Loop 才能重新指定一个 Mode。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。


而一个 Source 对象是一个事件,Source 有两个版本:Source0 和 Source1,Source0 只包含一个函数指针,并不能主动触发,需要将 Source0 标记为待处理,在 RunLoop 运转的时候,才会处理这个事件(如果 RunLoop 处于休眠状态,则不会被唤醒去处理),而 Source1 包含了一个 mach_port 和一个函数指针,mach_port 是 iOS 系统提供的基于端口的输入源,可用于线程或进程间通讯。而 RunLoop 支持的输入源类型中就包括基于端口的输入源,可以做到对 mach_port 端口源事件的监听。所以监听到 source1 端口的消息时,RunLoop 就会自己醒来去执行 Source1 事件(也能称为被消息唤醒)。也就是 Source0 是直接添加给 RunLoop 处理的事件,而 Source1 是基于端口的,进程或线程之间传递消息触发的事件(为什么要 0 和 1 来命名,每次都记不住,GG)。


Timer 是基于时间的触发器,CFRunLoopTimerRef 和 NSTimer 可以通过 Toll-free bridging 技术混用,Toll-free bridging 是一种允许某些 ObjC 类与其对应的 CoreFoundation 类之间可以互换使用的机制,当将 Timer 加入到 RunLoop 时,RunLoop 会注册对应的时间点,当时间点到时,RunLoop 会被唤醒以执行 Timer 回调。


Observer(观察者)都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:


typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};


也就是可以在这几个时机去安排 RunLoop 执行一些其他的任务。


上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。


RunLoop 的 CFRunLoopMode 和 CFRunLoop 的结构大致如下:


struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};

struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};


这里有个概念叫 "CommonModes":一个 Mode 可以将自己标记为"Common"属性(通过将其 ModeName 添加到 RunLoop 的 "commonModes" 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 "Common" 标记的所有Mode里。


如上文说的 RunLoop 一次循环只能运行在一个 Mode 下,是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。但如果一个 Source/Timer/Observer 想在多个 Mode 下运作,则可以分别加入到多个 Mode,或者给两个 Mode 添加 "Common" 标记,再将 Source/Timer/Observer 加入到 RunLoop 的 "commonModeItems" 。


应用场景举例:主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为"Common"属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,如果想让它回调则可以将这个 Timer 分别加入这两个 Mode。或将 Timer 加入到顶层的 RunLoop 的 "commonModeItems" 。


CFRunLoop 对外暴露的管理 Mode 接口只有下面2个:


CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);


Mode 暴露的管理 mode item 的接口有下面几个:


CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);


可以注意到 mode 并不像 Source/Timer/Observer 一样有 Remove 方法,所以 mode 只能增加,不能减少。


当你传入一个新的 mode name 但 RunLoop 内部没有对应 mode 时,RunLoop会自动帮你创建对应的 CFRunLoopModeRef。


接下来看 RunLoop 的执行逻辑


按 1 - 10 来理理,首先要知道 Observer 是观察者,也就是下面这几种状态都会通知观察者,开发者也添加一个观察者,去在以下几种状态的时候,执行一些任务,比如将没啥实时性要求的东西,在即将进入休眠状态时执行。
    kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop

其中第 2 步虽然通知 Observer 即将处理 Timer,但其实并没有真的即将处理 Timer 回调,这个通知每次 Loop 循环都会调用,但 Timer 只有在注册的时间到了才会在第 9 步去执行,第 4 步处理运行的 mode 里待处理的 Source0,其中第 5 步会判断 mode item 里是否有 Source1 处于 ready 状态(也就是 Source1 的端口已经收到消息),有的话跳到第 9 步,处理 Source1 事件,然后进入下一个循环,没有的话说明 mode item 里的事件都处理完毕,线程进入休眠状态,等待 Source1,Timer 或者外部手动将 RunLoop 唤醒(上文说 Source0 并不能唤醒 RunLoop,所以一般会通过手动唤醒 RunLoop,来让 RunLoop 处理新加入进去的 Source0)。


可以看到,实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里,去执行加入到 RunLoop 里的 Source0,Timer 的回调,以及 Observer 回调,以及用于线程或进程间通讯的 Source1,当所有的都处理完之后,结束一次循环,进入休眠状态,休眠的时候等待 Timer 注册的时间点或者 Source1 唤醒 RunLoop(也可以手动唤醒)。


从上面我们可以了解,线程和进程之间的通讯是基于 mach port 传递消息实现的,这也是 RunLoop 的核心。有必要了解一下 mach port, OSX/iOS 的系统架构分为 4 层,从外到内为应用层,应用框架层,核心框架层,Darwin。应用层包括用户能接触到的图形应用,应用框架层即开发人员接触到的 Cocoa 等框架,核心框架层包括各种核心框架、OpenGL 等内容,Darwin 即操作系统的核心,包括系统内核、驱动、Shell 等内容。

Darwin 核心的架构:


其中,在硬件层上面的三个组成部分:Mach、BSD、IOKit (还包括一些上面没标注的内容),共同组成了 XNU 内核。


XNU 内核的内环被称作 Mach,其作为一个微内核,仅提供了诸如处理器调度、IPC (进程间通信)等非常少量的基础服务。


BSD 层可以看作围绕 Mach 层的一个外环,其提供了诸如进程管理、文件系统和网络等功能。

IOKit 层是为设备驱动提供了一个面向对象(C++)的一个框架。


在 Mach 中,进程、线程和虚拟内存都被称为"对象"。Mach 的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。"消息"是 Mach 中最基础的概念,消息在两个端口 (port) 之间传递,这就是 Mach 的 IPC (进程间通信) 的核心。


Mach 中的对象通过一个 Mach 端口发送一个消息,消息中会携带目标端口,这个消息会从用户空间传递到内核空间,再由内核空间传递到目标端口,实现线程或进程之间的通讯。(也就是线程或进程之间的通讯不能绕过系统内核)。目标端口接收到消息,因为 RunLoop 会对 mach_port 端口源进行监听,如果 RunLoop 此时处于休眠状态,则被唤醒,便可以处理已经接收到消息的 source1 事件。

RunLoop 实现了很多功能


启动后,系统默认注册了5个Mode:


1:kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。


2:UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。


3:UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。


4:GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。


5:kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。


AutoreleasePool


App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()(因为需要设置不同的优先级,所以注册两个)。


第一个 Observer 监视的事件是 Entry(即将进入Loop),用来创建自动释放池,且设置的优先级最高,保证创建释放池发生在其他所有回调之前。


第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时去释放旧的池并创建新池;Exit(即将退出Loop) 时释放自动释放池。这个 Observer 的优先级最低,保证其释放池子发生在其他所有回调之后。


在主线程执行的代码,通常是写在诸如事件回调、Timer 回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏。


事件响应


苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。


当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的事件传递。


_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。


手势识别


当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。


苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个 Observer 的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行 GestureRecognizer 的回调。


当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。


界面更新


当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay 方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。


苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。也就是 UI 是在界面 RunLoop 休眠之前更新的,所以如果想在 UI 更新之后做一些事情,可以注册一个 Observer 监听 kCFRunLoopAfterWaiting(刚从休眠中唤醒)。

定时器


NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。


如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。


CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。


PerformSelecter


当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。


当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。


GCD


当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。


网络请求


iOS 中,关于网络请求的接口自下至上有如下几层:


CFSocket
CFNetwork ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession ->AFNetworking2, Alamofire


通常使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate 就会不停收到事件回调。实际上,start 这个函数的内部会会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了多个需要手动触发的 Source0。


当开始网络传输时,NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 基于 mach port 的 Source1 接收来自底层 CFSocket 的消息,当收到消息后,其会在合适的时机将 Source0 标记为待处理,同时唤醒 Delegate 线程的 RunLoop 来让其处理 Source0。完成一个由 CFSocket 线程到网络请求所在线程的数据处理。

AFNetworking


AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 创建了一个线程,并在这个线程中启动了一个 RunLoop,RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。当需要这个后台线程执行网络请求任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了该线程的 RunLoop 中。


AsyncDisplayKit


AsyncDisplayKit 是 Facebook 推出的用于保持界面流畅性的框架,其原理大致如下:


UI 线程中一旦出现繁重的任务就会导致界面卡顿,这类任务通常分为3类:排版,绘制,UI对象操作。


排版通常包括计算视图大小、计算文本高度、重新计算子式图的排版等操作。

绘制一般有文本绘制 (例如 CoreText)、图片绘制 (例如预先解压)、元素绘制 (Quartz)等操作。

UI对象操作通常包括 UIView/CALayer 等 UI 对象的创建、设置属性和销毁。


其中前两类操作可以通过各种方法扔到后台线程执行,而最后一类操作只能在主线程完成,并且有时后面的操作需要依赖前面操作的结果 (例如TextView创建时可能需要提前计算出文本的大小)。ASDK 所做的,就是尽量将能放入后台的任务放入后台,不能的则尽量推迟 (例如视图的创建、属性的调整)。

为此,ASDK 创建了一个名为 ASDisplayNode 的对象,并在内部封装了 UIView/CALayer,它具有和 UIView/CALayer 相似的属性,例如 frame、backgroundColor 等。所有这些属性都可以在后台线程更改,开发者可以只通过 Node 来操作其内部的 UIView/CALayer,这样就可以将排版和绘制放入了后台线程。但是无论怎么操作,这些属性总需要在某个时刻同步到主线程的 UIView/CALayer 去。


ASDK 仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务(需要将 Node 的属性同步到主线程的 UIView/CALayer 去的任务),然后一一执行。


总结:


ibireme深入理解RunLoop 前前后后我看了好几遍,每次都看的不深入,总是瞬时了解,睡一觉就记不清楚细节了,这次自己逐行去理解,并写下来,加上点自己的理解,也写了点代码去观察 RunLoop,算是对 RunLoop 的理解更加深入了。


作者:Cooci
原帖链接:https://www.jianshu.com/p/2103d15f4423

收起阅读 »

Runtime的底层原理及应用

Runtime 是一个运行时库,主要使用 C 和汇编写的库,为 C 添加了面向对象的能力并创造了 Objective-C,并且拥有消息分发,消息转发等功能。也就是 Runtime 涉及三个点,面向对象消息分发消息转发。面向对象:Objective-C 的对象是...
继续阅读 »

Runtime 是一个运行时库,主要使用 C 和汇编写的库,为 C 添加了面向对象的能力并创造了 Objective-C,并且拥有消息分发,消息转发等功能。



也就是 Runtime 涉及三个点,

  • 面向对象
  • 消息分发
  • 消息转发

面向对象:

Objective-C 的对象是基于 Runtime 创建的结构体。先从代码层面分析一下。

Class *class = [[Class alloc] init];

alloc 方法会为对象分配一块内存空间,空间的大小为 isa_t(8 字节)的大小加上所有成员变量所需的空间,再进行一次内存对齐。分配完空间后会初始化 isa_t ,而 isa_t 是一个 union 类型的结构体(或者称之为联合体),它的结构是在 Runtime 里被定义的。


union isa_t {  
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }

Class cls;
uintptr_t bits;

struct {
uintptr_t indexed : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
};
};


从 isa_t 的结构可以看出,isa_t 可以存储 struct,uintptr_t 或者 Class 类型


init 方法就直接返回了初始化好的对象,class 指针指向这个初始化好的对象。


也就是在 Runtime 的协助之下,一个对象完成了创建。


你可能想知道,这个对象只存放了一个 isa_t 结构体和成员变量,对象的方法在哪里?


在编译的时候,类在内存中的位置就已经确定,而在 main 方法之前,Runtime 将可执行文件中和动态库所有的符号(Class,Protocol,Selector,IMP,…)加载到内存中,由 Runtime 管理,这里也包括了也是一个对象的类。


类对象里储存着一个 isa_t 的结构体,super_class 指针,cache_t 结构体,class_data_bits_t 指针。


struct objc_class : objc_object {
isa_t isa;
Class superclass;
cache_t cache;
class_data_bits_t bits;



class_data_bits_t 指向类对象的数据区域,数据区域存放着这个类的实例方法链表。而类方法存在元类对象的数据区域。也就是有对象,类对象,元类对象三个概念,对象是在运行时动态创建的,可以有无数个,类对象和元类对象在 main 方法之前创建的,分别只会有一个。


消息分发


在 Objective-C 中的“方法调用”其实应该叫做消息传递,[object message] 会被编译器翻译为 objc_msgSend(object, @selector(message)),这是一个 C 方法,首先看它的两个参数,第一个是 object ,既方法调用者,第二个参数称为选择子 SEL,Objective-C 为我们维护了一个巨大的选择子表,在使用 @selector() 时会从这个选择子表中根据选择子的名字查找对应的 SEL。如果没有找到,则会生成一个 SEL 并添加到表中,在编译期间会扫描全部的头文件和实现文件将其中的方法以及使用 @selector() 生成的选择子加入到选择子表中。


通过第一个参数 object,可以找到 object 对象的 isa_t 结构体,从上文中能看 isa_t 结构体的结构,在 isa_t 结构体中,shiftcls 存放的是一个 33 位的地址,用于指向 object 对象的类对象,而类对象里有一个 cache_t 结构体,来看一下 cache_t 的具体代码


struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;



_mask:分配用来缓存 bucket 的总数。

_occupied:表明目前实际占用的缓存 bucket 的个数。

_buckets:一个散列表,用来方法缓存,bucket_t 类型,包含 key 以及方法实现 IMP。


struct bucket_t {
private:
cache_key_t _key;
IMP _imp;



objc_msgSend() 方法会先从缓存表里,查找是否有该 SEL 对应的 IMP,有的话算命中缓存,直接通过函数指针 IMP ,找到方法的具体实现函数,执行。


当然缓存表里可能并不会命中,则此时会根据类对象的 class_data_bits_t 指针找到数据区域,数据区域里用链表存放着类的实例方法,实例方法也是一个结构体,其结构为:


struct method_t {  
SEL name;
const char *types;
IMP imp;
};


编译器将每个方法的返回值和参数类型编码为一个字符串,types 指向的就是这样一个字符串,objc_msgSend() 会在类对象的方法链表里按链表顺序去匹配 SEL,匹配成功则停止,并将此方法加入到类对象的 _buckets 里缓存起来。如果没找到则会通过类对象的 superclass 指针找到其父类,去父类的方法列表里寻找(也会从父类的方法缓存列表开始)。


如果继续没有找到会一直向父类寻找,直到遇见 NSObject,NSObject 的 superclass 指向 nil。也就意味着寻找结束,并没有找到实现方法。(如果这个过程找到了,也同样会在 object 的类对象的 _buckets 里缓存起来)。


选择子在当前类和父类中都没有找到实现,就进入了方法决议(method resolve),首先判断当前 object 的类对象是否实现了 resolveInstanceMethod: 方法,如果实现的话,会调用 resolveInstanceMethod:方法,这个时候我们可以在 resolveInstanceMethod:方法里动态的添加该 SEL 对应的方法(也可以去做点别的,比如写入日志)。之后会重新执行查找方法实现的流程,如果依旧没找到方法,或者没有实现 resolveInstanceMethod: 方法,Runtime 还有另一套机制,消息转发。


消息转发


消息转发分为以下几步:


1.调用 forwardingTargetForSelector: 方法,尝试找到一个能响应该消息的对象。如果获取到,则直接转发给它。如果返回了 nil,继续下面的动作。


2.调用 methodSignatureForSelector: 方法,尝试获得一个方法签名。如果获取不到,则直接调用 doesNotRecognizeSelector 抛出异常。


3.调用 forwardInvocation: 方法,将第 2 步获取到的方法签名包装成 Invocation 传入,如何处理就在这里面了。


以上三个方法都可以通过在 object 的类对象里实现, forwardingTargetForSelector: 可以通过对参数 SEL 的判断,返回一个可以响应该消息的对象。这样则会重新从该对象开始执行查找方法实现的流程,找到了也同样会在 object 的类对象的 _buckets 里缓存起来。而 2,3 方法则一般是配套使用,实现 methodSignatureForSelector: 方法根据参数 SEL ,做相应处理,返回 NSMethodSignature (方法签名) 对象,NSMethodSignature 对象会被包装成 NSInvocation 对象,forwardInvocation: 方法里就可以对 NSInvocation 进行处理了。


上面是讲的是实例方法,类方法没什么区别,类方法储存在元类对象的数据区域里,通过类对象的 isa_t 找到元类对象,执行查找方法实现的流程,元类对象的 superclass 最终也会指向 NSObject。没找到的话,也会有方法决议以及消息转发。


runtime 可以做什么:


实现多继承:从 forwardingTargetForSelector: 方法就能知道,一个类可以做到继承多个类的效果,只需要在这一步将消息转发给正确的类对象就可以模拟多继承的效果。


交换两个方法的实现


    Method m1 = class_getInstanceMethod([M1 class], @selector(hello1));
Method m2 = class_getInstanceMethod([M2 class], @selector(hello2));
method_exchangeImplementations(m2, m1);


关联对象


通过下面两个方法,可以给 category 实现添加成员变量的效果。


objc_setAssociatedObject
objc_getAssociatedObject


动态添加类和方法:


objc_allocateClassPair 函数与 objc_registerClassPair 函数可以完成一个新类的添加,class_addMethod 给类添加方法,class_addIvar 添加成员变量,objc_registerClassPair 来注册类,其中成员变量的添加必须在类注册之前,类注册后就可以创建该类的对象了,而再添加成员变量就会破坏创建的对象的内存结构。


将 json 转换为 model


用到了 Runtime 获取某一个类的全部属性的名字,以及 Runtime 获取属性的类型。



作者:Cooci
原帖链接:https://www.jianshu.com/p/2dae81846046
收起阅读 »

Android日记之View的绘制流程(二)

Android日记之View的绘制流程(一)Measure过程Measure过程要分情况来看,如果只是原始的View,那么通过measure()方法就完成了测量的过程。如果是一个ViewGroup,除了完成自己的测量过程外,还要遍历去调用子元素的measure...
继续阅读 »


Android日记之View的绘制流程(一)

Measure过程

Measure过程要分情况来看,如果只是原始的View,那么通过measure()方法就完成了测量的过程。如果是一个ViewGroup,除了完成自己的测量过程外,还要遍历去调用子元素的measure(),下面分两种情况进行讨论。

View的measure过程

View的measure过程就是会调用View的measure()方法,它是一个final类型的方法,在measure()方法还会调用View的onMeasure()方法,我们看这个方法写了什么。


protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

代码很少,这里的setMeasuredDimension()方法会设置View的宽和高的测量值,因此我们只要看getDefaultSize()即可。


public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}

从源码可以看出,逻辑还是很简单的,对于我们来说,只需要看AT_MOST和EXACTLY这两种情况,简单理解,其实getDefaultSize()返回的大小就是measureSpec中的specSize,而这个specSize就是View测量后的大小,这里多次提到测量后的大小,是因为View最终得大小是在layout阶段确定的,所以这里必须要区分,但几乎所有情况下View的测量大小和最终大小是相等的。

至于UNSPECIFIED这种情况,一般用于系统内部的测量过程,在这种情况下,View的大小为getDefaultSize()的第一个参数size,即宽和高为getSuggestedMinimunWidth()getSuggestedMinimunHeight()的返回值:


protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

这里就只分析getSuggestedMinimumWidth()方法的实现,getSuggestedMinimunHeight()和它的实现原理是一样的,从代码中可以看出,如果View没有设置背景,那么宽度就为mMinWidth,而mMinWidth对应android:Width这个属性所指定的值。如果不指定,那么mMinWidth则默认为0,如果设置了背景,则View的宽度为max(mMinWidth, mBackground.getMinimumWidth()。那mBackground.getMinimumWidth()是什么呢?



public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

可以看出,getMinimumWidth()返回的就是Drawable的原始宽度,前提是这个Drawable有原始宽度,否则就返回0。这里说明一下,ShapeDrawable无原始宽和高,BitmapDrawab有原始宽和高。

从getDefaultSize可以看出,View的宽和高由specSize决定。直接继承View的自定义控件需要重写onMeasure()方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于match_parent。

ViewGroup的measure过程

对于ViewGroup来说,除了完成自己的mesaure过程以外,还会遍历去调用所有子元素的measure()方法,各个子元素在递归的去执行这个过程,和View不同的是,ViewGroup时候是一个抽象类,因此它没有重写View的onMeasure(),但是它提供了一个叫measureChildren()的方法。


protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}


protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec)
{
final LayoutParams lp = child.getLayoutParams();

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

measureChildren()会调用measureChild()方法,在measureChild()方法里,就是去除子元素的LayoutParams,然后在通过getChildMeasureSpec()来创建子元素的MeasureSpec,接着将MeasureSpec直接传递给View的measure()方法进行测量。

我们知道,ViewGroup并没有定义其测量的具体过程,这是因为ViewGroup是一个抽象类,其测量过程的onMeasure()方法需要由各个子类去具体实现,比如LinearLayout、RelativeLayout等,ViewGroup不做统一的onMeasure()实现,是因为不同的ViewGroup子类有不同的布局特性,导致它们的测量细节个不不相同,比如LinearLayout和RelativeLayout这两者的布局特性显然不同,因为无法做统一,以后会专门写一篇文章来讲解每个layout的布局是怎么实现的。

Layout过程

Layout的作用就是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用layout()方法,在layout()方法中onLayout()方法又会被调用。layout过程相比measure过程就简单多了,layout()方法确定View本身的位置,而onLayout()方法则会确定所有子元素的位置,先看View的layout()方法。


public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}

int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;

boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);

if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}

mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList listenersCopy =
(ArrayList)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}

final boolean wasLayoutValid = isLayoutValid();

mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;

if (!wasLayoutValid && isFocused()) {
mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
if (canTakeFocus()) {
// We have a robust focus, so parents should no longer be wanting focus.
clearParentsWantFocus();
} else if (getViewRootImpl() == null || !getViewRootImpl().isInLayout()) {
// This is a weird case. Most-likely the user, rather than ViewRootImpl, called
// layout. In this case, there's no guarantee that parent layouts will be evaluated
// and thus the safest action is to clear focus here.
clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
clearParentsWantFocus();
} else if (!hasParentWantsFocus()) {
// original requestFocus was likely on this view directly, so just clear focus
clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
}
// otherwise, we let parents handle re-assigning focus during their layout passes.
} else if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) {
mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
View focused = findFocus();
if (focused != null) {
// Try to restore focus as close as possible to our starting focus.
if (!restoreDefaultFocus() && !hasParentWantsFocus()) {
// Give up and clear focus once we've reached the top-most parent which wants
// focus.
focused.clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
}
}
}

if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}

首先会通过setFrame()方法来设定View的四个顶点的位置,即初始化mLeft、mRight、mTop和mBottom这四个值,View的四个顶点一旦确定,那么View在父容器的位置也就确定了。接着就会调用onLayout()方法,这个方法的用途是父容器确定子元素的位置,和onMeasure()类似,onLayout()的具体实现同样和具体的布局有关,所以View和ViewGroup都没有真正实现onLayout()方法。

Draw过程

Draw过程就简单很多了,作用就是将View会知道屏幕上面。View的绘制过程遵循着如下几步。
(1)background.draw(canvas) - 绘制背景
(2)onDraw() - 绘制自己
(3)dispatchDraw()- 绘制children
(4)onDrawScrollBars() - 绘制装饰

绘制流程从ViewRootImpl的performDraw()开始,performDraw()方法在类 ViewRootImpl内,其核心代码如下:

private void performDraw() {



boolean canUseAsync = draw(fullRedrawNeeded);
}

private boolean draw(boolean fullRedrawNeeded) {
...
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
scalingRequired, dirty, surfaceInsets)) {
return false;
}
}

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty, Rect surfaceInsets)
{
...
mView.draw(canvas);
...
}

然后查看View的draw()方法的源代码:

public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/

// Step 1, draw the background, if needed
int saveCount;

if (!dirtyOpaque) {
drawBackground(canvas);
}

// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);

// Step 4, draw the children
dispatchDraw(canvas); //这里进行传递的实现!!!

drawAutofilledHighlight(canvas);

// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}

// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);

// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);

if (debugDraw()) {
debugDrawFocus(canvas);
}

// we're done...
return;
}

......
}


View的绘制过程的传递是通过dispatchDraw()来实现的,dispatchDraw()会遍历调用所有子元素的draw()方法,如此一来draw事件就一层层的传递了下去。这里补充一下View还有一个特殊的方法叫setWillNotDraw(),代码如下:

/**
* If this view doesn't do any drawing on its own, set this flag to
* allow further optimizations. By default, this flag is not set on
* View, but could be set on some View subclasses such as ViewGroup.
*
* Typically, if you override {@link #onDraw(android.graphics.Canvas)}
* you should clear this flag.
*
* @param willNotDraw whether or not this View draw on its own
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}


如果一个View不需要绘制任何内容,那么设置这个标记位为true以后,系统会进行相应的优化,默认情况下,View没有启用这个优化标记位,但是ViewGroup会默认启用这个标记位。这个标记位对开发的实际意义是:当我们的自定义控件继承与ViewGroup并且本身不具备绘制功能时,就可以开启这个标记位进行后续的优化。当然如果确定ViewGroup要绘制内容的话,就要关闭这个标记位。



作者:居居居居居居x
链接:https://www.jianshu.com/p/f4afebd50a2b
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »