环信即时通讯云

环信即时通讯云

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

环信开发文档

环信客服云

环信客服云

无需下载,注册即用
声网开发者社区

声网开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Android基础到进阶UI CheckedTextView 使用+实例

CheckedTextView是什么 CheckedTextView继承自TextView且实现了Checkable接口,对TextView界面和显示进行了扩展的控件,支持Checkable。可以实现 单选或多选功能,在你懒得使用两者结合的时候,这就是不二选...
继续阅读 »

CheckedTextView是什么


CheckedTextView继承自TextView且实现了Checkable接口,对TextView界面和显示进行了扩展的控件,支持Checkable。可以实现 单选多选功能,在你懒得使用两者结合的时候,这就是不二选择。


主要XML属性如下:


android:checkMark 按钮样式。



  • 默认单选框样式:android:checkMark="?android:attr/listChoiceIndicatorSingle"

  • 默认复选框样式:android:checkMark="?android:attr/listChoiceIndicatorMultiple"

  • 当然也可以使用drawable自定义样式


android:checkMarkTint 按钮的颜色。


android:checkMarkTintMode 混合模式按钮的颜色。


android:checked 初始选中状态,默认false。


在点击事件里判断状态设置状态


CheckedTextView.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        CheckedTextView.toggle();//切换选中与非选中状态
    }
});

咱们看看CheckedTextView.toggle()是干嘛的


public void toggle() {
    setChecked(!mChecked);
}

就是实现这个控件的状态反设置。


第一次点击无效


android:focusableInTouchMode="true",这个属性加上会导致第一次点击触发不了选择事件。


实例


官方文档指出,「结合ListView使用更佳」,咱下面通过一个栗子了解一下,下面是效果图:



1.主界面CheckedTextViewActivity.java


public class CheckedTextViewActivity extends AppCompatActivity {
    private ListView lv_ctv_multiple,lv_ctv_single;
    private CtvMultipleAdapter ctvAdapter;
    private TextView tv_multiple_title,tv_single_title;
    private CtvSingleAdapter ctvSingleAdapter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_textview_ctv);//加载布局文件
        initView();
    }
    private void initView() {
        ArrayList<String> ctvString = new ArrayList<>();
        ctvString.add("秦始皇嬴政");
        ctvString.add("汉高祖刘邦");
        ctvString.add("唐太宗李世民");
        ctvString.add("宋太祖赵匡胤");
        //复选
        lv_ctv_multiple = findViewById(R.id.lv_ctv_multiple);
        tv_multiple_title = findViewById(R.id.tv_multiple_title);
        ctvAdapter = new CtvMultipleAdapter(this,ctvString,tv_multiple_title);
        lv_ctv_multiple.setAdapter(ctvAdapter);
        //设置Item间距
        lv_ctv_multiple.setDividerHeight(0);

        //单选
        lv_ctv_single = findViewById(R.id.lv_ctv_single);
        tv_single_title = findViewById(R.id.tv_single_title);
        ctvSingleAdapter = new CtvSingleAdapter(this,ctvString,tv_single_title);
        lv_ctv_single.setAdapter(ctvSingleAdapter);
        //设置Item间距
        lv_ctv_single.setDividerHeight(0);

    }
}

2.主布局activity_textview_ctv.xml


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

    <TextView
        android:id="@+id/tv_multiple_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="@dimen/dimen_10"
        android:textColor="@color/black"
        android:text="复选"
        android:textSize="@dimen/text_size_16" />

    <ListView
        android:id="@+id/lv_ctv_multiple"
        android:layout_width="match_parent"
        android:layout_height="180dp" />


    <TextView
        android:id="@+id/tv_single_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="@dimen/dimen_10"
        android:text="单选"
        android:textColor="@color/color_188FFF"
        android:layout_marginTop="@dimen/dimen_10"
        android:textSize="@dimen/text_size_20" />

    <ListView
        android:id="@+id/lv_ctv_single"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

3.复选框Adapter


public class CtvMultipleAdapter extends BaseAdapter {
    private LayoutInflater mInflater;//得到一个LayoutInfalter对象用来导入布局
    private List<String> list;
    private TextView tvTitle;
    private List<String> selectList = new ArrayList<>();
    public CtvMultipleAdapter(Context context, List<String> list, TextView tv) {
        this.mInflater = LayoutInflater.from(context);
        this.list = list;
        tvTitle = tv;
    }

    @Override
    public int getCount() {
        return list.size();
    }

    @Override
    public Object getItem(int position) {
        return null;
    }

    @Override
    public long getItemId(int position) {
        return 0;
    }

    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        final CtvViewHolder holder;
        final String string = list.get(position);
        //观察convertView随ListView滚动情况
        if (convertView == null) {
            convertView = mInflater.inflate(R.layout.item_ctv_multiple, null);
            holder = new CtvViewHolder();
            /*得到各个控件的对象*/
            holder.ctv_top = (CheckedTextView) convertView.findViewById(R.id.ctv_top);
            convertView.setTag(holder);//绑定ViewHolder对象
        } else {
            holder = (CtvViewHolder) convertView.getTag();//取出ViewHolder对象
        }
        holder.ctv_top.setText(string);
        //默认选中状态
        if(holder.ctv_top.isChecked()){
            //list未包含选中string;
            if(!selectList.contains(string)){
                selectList.add(string);
            }
        }
        if (selectList.size() == 0) {
            tvTitle.setText("");
        } else {
            tvTitle.setText(selectList.toString());
        }

        holder.ctv_top.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                holder.ctv_top.toggle();//切换选中与非选中状态
                //单选
                if(holder.ctv_top.isChecked()){//
                    //list未包含选中string;
                    if(!selectList.contains(string)){
                        selectList.add(string);
                    }
                }else{
                    //list未包含选中string;
                    if(selectList.contains(string)){
                        selectList.remove(string);
                    }
                }
                if (selectList.size() == 0) {
                    tvTitle.setText("");
                } else {
                    tvTitle.setText(selectList.toString());
                }

            }
        });
        return convertView;
    }
    /*存放控件*/
    public class CtvViewHolder {
        public CheckedTextView ctv_top;
    }

    @Override
    public boolean areAllItemsEnabled() {
        return false;//Item不可点击
    }

    @Override
    public boolean isEnabled(int position) {
        return false;//Item不可点击
        // 拦截事件交给上一级处理
        //return super.isEnabled(position);
    }

}

4.复选框adapter对应布局


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

    <CheckedTextView
        android:id="@+id/ctv_top"
        android:checked="true"
        android:checkMark="?android:attr/listChoiceIndicatorMultiple"
        android:checkMarkTint="@color/color_FF773D"
        android:padding="10dp"
        android:textSize="16sp"
        android:layout_marginTop="3dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</LinearLayout>

这里用到checkMark(默认复选框样式)、checkMarkTint(复选框颜色设为黄色)、和checked(true默认选中)几个属性,可以更好的理解他们。


5.单选框adapter


  private String selectStr="";//全局变量
        holder.ctv_top.setText(string);
        holder.ctv_top.setChecked(selectStr.equals(string));
        holder.ctv_top.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                holder.ctv_top.toggle();//切换选中与非选中状态
                //单选
                if(holder.ctv_top.isChecked()){
                    selectStr=string;
                }else{
                    selectStr="";
                }
                tvTitle.setText(selectStr);
                notifyDataSetChanged();
            }
        });

大部分与复选框CtvMultipleAdapter设置相同,仅部分不同就不做多重复了。


6.单选框adapter对应布局


 <CheckedTextView
        android:id="@+id/ctv_top"
        android:checkMark="?android:attr/listChoiceIndicatorSingle"
        android:padding="10dp"
        android:textSize="16sp"
        android:layout_marginTop="3dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />


仅使用单选默认样式。


7.逻辑处理从adapter放在主界面处理


  ListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                //在这里进行单选复选的逻辑处理
            }
        });

使用CheckedTextView配合ListView实现单选与多选的功能我们实现了。到这里,关于CheckedTextView我们也就介绍完了,嘿嘿。



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

Binder概述,快速了解Binder体系

前言 众所周知,Binder是Android系统中最主要的进程间通信套件,更具体一点,很多文章称之为Binder驱动,那为什么说它是一个驱动呢,驱动又是何物,让我们自底向上,从内核中的Binder来一步步揭开它的面纱。本文重点在帮助读者对于Binder系统有...
继续阅读 »

前言


众所周知,Binder是Android系统中最主要的进程间通信套件,更具体一点,很多文章称之为Binder驱动,那为什么说它是一个驱动呢,驱动又是何物,让我们自底向上,从内核中的Binder来一步步揭开它的面纱。本文重点在帮助读者对于Binder系统有一个简略的了解,所以写得比较笼统,后续文章会详细分析。


Binder到底是什么


Android系统内核是Linux,每个进程有自己的虚拟地址空间,在32位系统下最大是4GB,其中3GB为用户空间,1GB为内核空间;每个进程用户空间相对独立,而内核空间是一样的,可以共享,如下图


地址空间.png


Linux驱动运行在内核空间,狭义上讲是系统用于控制硬件的中间程序,但是归根结底它只是一个程序一段代码,所以具体实现并不一定要和硬件有关。Binder就是将自己注册为一个misc类型的驱动,不涉及硬件操作,同时自身运行于内核中,所以可以当作不同进程间的桥梁实现IPC功能。


Linux最大的特点就是一切皆文件,驱动也不例外,所有驱动都会被挂载在文件系统dev目录下,Binder对应的目录是/dev/binder,注册驱动时将open release mmap等系统调用注册到Binder自己的函数,这样的话在用户空间就可以通过系统调用以访问文件的方式使用Binder。下面来粗略看一下相关代码。


device_initcall函数用于注册驱动,由系统调用


binder_init中调用misc_register注册一个名为binder的misc驱动,同时指定函数映射,将binder_open映射到系统调用open,这样就可以通过open("/dev/binder")来调用binder_open函数了


drivers/android/binder.c


// 驱动函数映射
static const struct file_operations binder_fops = {
.owner = THIS_MODULE,
.poll = binder_poll,
.unlocked_ioctl = binder_ioctl,
.compat_ioctl = binder_ioctl,
.mmap = binder_mmap,
.open = binder_open,
.flush = binder_flush,
.release = binder_release,
};

// 注册驱动参数结构体
static struct miscdevice binder_miscdev = {
.minor = MISC_DYNAMIC_MINOR,
// 驱动名称
.name = "binder",
.fops = &binder_fops
};

static int binder_open(struct inode *nodp, struct file *filp){......}
static int binder_mmap(struct file *filp, struct vm_area_struct *vma){......}

static int __init binder_init(void)
{
int ret;
// 创建名为binder的单线程的工作队列
binder_deferred_workqueue = create_singlethread_workqueue("binder");
if (!binder_deferred_workqueue)
return -ENOMEM;
......
// 注册驱动,misc设备其实也就是特殊的字符设备
ret = misc_register(&binder_miscdev);
......
return ret;
}
// 驱动注册函数
device_initcall(binder_init);

Binder的简略通讯过程


一个进程如何通过binder和另一个进程通讯?最简单的流程如下



  1. 接收端进程开启一个专门的线程,通过系统调用在binder驱动(内核)中先注册此进程(创建保存一个bidner_proc),驱动为接收端进程创建一个任务队列(biner_proc.todo)

  2. 接收端线程开始无限循环,通过系统调用不停访问binder驱动,如果该进程对应的任务队列有任务则返回处理,否则阻塞该线程直到有新任务入队

  3. 发送端也通过系统调用访问,找到目标进程,将任务丢到目标进程的队列中,然后唤醒目标进程中休眠的线程处理该任务,即完成通讯


在Binder驱动中以binder_proc结构体代表一个进程,binder_thread代表一个线程,binder_proc.todo即为进程需要处理的来自其他进程的任务队列


struct binder_proc {
// 存储所有binder_proc的链表
struct hlist_node proc_node;
// binder_thread红黑树
struct rb_root threads;
// binder_proc进程内的binder实体组成的红黑树
struct rb_root nodes;
......
}

Binder的一次拷贝


众所周知Binder的优势在于一次拷贝效率高,众多博客已经说烂了,那么什么是一次拷贝,如何实现,发生在哪里,这里尽量简单地解释一下。


上面已经说过,不同进程通过在内核中的Binder驱动来进行通讯,但是用户空间和内核空间是隔离开的,无法互相访问,他们之间传递数据需要借助copy_from_user和copy_to_user两个系统调用,把用户/内核空间内存中的数据拷贝到内核/用户空间的内存中,这样的话,如果两个进程需要进行一次单向通信则需要进行两次拷贝,如下图。


2copy.png


Binder单次通信只需要进行一次拷贝,因为它使用了内存映射,将一块物理内存(若干个物理页)分别映射到接收端用户空间和内核空间,达到用户空间和内核空间共享数据的目的。


发送端要向接收端发送数据时,内核直接通过copy_from_user将数据拷贝到内核空间映射区,此时由于共享物理内存,接收进程的内存映射区也就能拿到该数据了,如下图。


binder_mmap.png


代码实现部分


用户空间通过mmap系统调用,调用到Binder驱动中binder_mmap函数进行内存映射,这部分代码比较难读,感兴趣的可以看一下。


drivers/android/binder.c


binder_mmap创建binder_buffer,记录进程内存映射相关信息(用户空间映射地址,内核空间映射地址等)


static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
int ret;
//内核虚拟空间
struct vm_struct *area;
struct binder_proc *proc = filp->private_data;
const char *failure_string;
// 每一次Binder传输数据时,都会先从Binder内存缓存区中分配一个binder_buffer来存储传输数据
struct binder_buffer *buffer;

if (proc->tsk != current)
return -EINVAL;
// 保证内存映射大小不超过4M
if ((vma->vm_end - vma->vm_start) > SZ_4M)
vma->vm_end = vma->vm_start + SZ_4M;
......
// 采用IOREMAP方式,分配一个连续的内核虚拟空间,与用户进程虚拟空间大小一致
// vma是从用户空间传过来的虚拟空间结构体
area = get_vm_area(vma->vm_end - vma->vm_start, VM_IOREMAP);
if (area == NULL) {
ret = -ENOMEM;
failure_string = "get_vm_area";
goto err_get_vm_area_failed;
}
// 指向内核虚拟空间的地址
proc->buffer = area->addr;
// 用户虚拟空间起始地址 - 内核虚拟空间起始地址
proc->user_buffer_offset = vma->vm_start - (uintptr_t)proc->buffer;
......
// 分配物理页的指针数组,数组大小为vma的等效page个数
proc->pages = kzalloc(sizeof(proc->pages[0]) * ((vma->vm_end - vma->vm_start) / PAGE_SIZE), GFP_KERNEL);
if (proc->pages == NULL) {
ret = -ENOMEM;
failure_string = "alloc page array";
goto err_alloc_pages_failed;
}
proc->buffer_size = vma->vm_end - vma->vm_start;

vma->vm_ops = &binder_vm_ops;
vma->vm_private_data = proc;
// 分配物理页面,同时映射到内核空间和进程空间,先分配1个物理页
if (binder_update_page_range(proc, 1, proc->buffer, proc->buffer + PAGE_SIZE, vma)) {
ret = -ENOMEM;
failure_string = "alloc small buf";
goto err_alloc_small_buf_failed;
}
buffer = proc->buffer;
// buffer插入链表
INIT_LIST_HEAD(&proc->buffers);
list_add(&buffer->entry, &proc->buffers);
buffer->free = 1;
binder_insert_free_buffer(proc, buffer);
// oneway异步可用大小为总空间的一半
proc->free_async_space = proc->buffer_size / 2;
barrier();
proc->files = get_files_struct(current);
proc->vma = vma;
proc->vma_vm_mm = vma->vm_mm;

/*pr_info("binder_mmap: %d %lx-%lx maps %p\n",
proc->pid, vma->vm_start, vma->vm_end, proc->buffer);*/

return 0;
}

binder_update_page_range 函数为映射地址分配物理页,这里先分配一个物理页(4KB),然后将这个物理页同时映射到用户空间地址和内存空间地址


static int binder_update_page_range(struct binder_proc *proc, int allocate,
void *start, void *end,
struct vm_area_struct *vma)

{
// 内核映射区起始地址
void *page_addr;
// 用户映射区起始地址
unsigned long user_page_addr;
struct page **page;
// 内存结构体
struct mm_struct *mm;

if (end <= start)
return 0;
......
// 循环分配所有物理页,并分别建立用户空间和内核空间对该物理页的映射
for (page_addr = start; page_addr < end; page_addr += PAGE_SIZE) {
int ret;
page = &proc->pages[(page_addr - proc->buffer) / PAGE_SIZE];

BUG_ON(*page);
// 分配一页物理内存
*page = alloc_page(GFP_KERNEL | __GFP_HIGHMEM | __GFP_ZERO);
if (*page == NULL) {
pr_err("%d: binder_alloc_buf failed for page at %p\n",
proc->pid, page_addr);
goto err_alloc_page_failed;
}
// 物理内存映射到内核虚拟空间
ret = map_kernel_range_noflush((unsigned long)page_addr,
PAGE_SIZE, PAGE_KERNEL, page);
flush_cache_vmap((unsigned long)page_addr,
// 用户空间地址 = 内核地址+偏移
user_page_addr =
(uintptr_t)page_addr + proc->user_buffer_offset;
// 物理空间映射到用户虚拟空间
ret = vm_insert_page(vma, user_page_addr, page[0]);
}
}

binder_mmap函数中调用binder_update_page_range只为映射区分配了一个物理页的空间,在Binder开始通讯时,会再通过binder_alloc_buf函数分配更多物理页,这是后话了。


Binder套件架构


内核层的Binder驱动已经提供了IPC功能,不过还需要在framework native层提供一些对于驱动层的调用封装,使framework开发者更易于使用,由此封装出了native Binder;同时,由于framework native层是c/c++语言实现,对于应用开发者,需要更加方便的Java层的封装,衍生出Java Binder;最后在此之上,为了减少重复代码的编写和规范接口,在Java Binder的基础上又封装出了AIDL。经过层层封装,在使用者使用AIDL时对于Binder基本上是无感知的。


这里贴一张架构图。


binder架构.png


Native层


BpBinder代表服务端Binder的一个代理,内部有一个成员mHandle就是服务端Binder在驱动层的句柄,客户端通过调用BpBinder::transact传入该句柄,经过驱动层和服务端BBinder产生会话,最后服务端会调用到BBinder::onTransact。在这里两者之间通过约定好的code来标识会话内容。


前面提到过,需要用Binder进行通信的进程都需要在驱动中先注册该进程,并且每次通讯时需要一个线程死循环读写binder驱动。驱动层中一个进程对应一个binder_proc,一个线程对应binder_thread;而在framework native层中,进程对应一个ProcessState,线程对应IPCThreadStateBpBinder::transact发起通讯最终也是通过IPCThreadState.transact调用驱动进行。


实际上Android中每个应用进程都打开了Binder驱动(在驱动中注册),Zygote进程在fork出应用进程后,调用app_main.cpp中onZygoteInit函数初始化,此函数中就创建了该进程的ProcessState实例,打开Binder驱动然后分配映射区,驱动中也创建并保存一个该进程的binder_proc实例。这里借一张图来描述。


Java层


Java层是对native层相关类的封装,BBinder对应Binder,BpBinder对应BinderProxy,java层最后还是会调用到native层对应函数


AIDL


AIDL生成的代码对于Binder进行了进一步封装,<接口>.Stub对应服务端Binder,<接口>.Stub.Proxy标识客户端,内部持有一个mRemote实例(BinderProxy),aidl根据定义的接口方法生成若干个TRANSACTION_<函数名> code常量,两端Binder通过这些code标识解析参数,调用相应接口方法。换言之AIDL就是对BinderProxy.transactBinder.onTransact进行了封装,使用者不必再自己定义每次通讯的code以及参数解析


后记


本篇文章主要为不了解Binder体系的读者提供一个笼统的认识,接下来的文章会从AIDL远程服务开始层层向下分析整个IPC过程,所以如果想要更深一步了解Binder,本文作为前置知识也比较重要。


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

手把手教你 Debug — iOS 14 ImageIO Crash 分析

背景去年 9 月份开始,许多用户升级到 iOS 14 之后,线上出现很多 ImageIO 相关堆栈的 Crash 问题,而且公司内几乎所有的 APP 上都有出现,在部分 APP上甚至达到了 Top 3  Crash。得益于 APM 平台精准数据采集机...
继续阅读 »

背景

去年 9 月份开始,许多用户升级到 iOS 14 之后,线上出现很多 ImageIO 相关堆栈的 Crash 问题,而且公司内几乎所有的 APP 上都有出现,在部分 APP上甚至达到了 Top 3  Crash。

得益于 APM 平台精准数据采集机制和丰富的异常信息现场,我们通过收集到详细的 Crash 日志信息进行分析解决。

问题定位

堆栈信息

从堆栈信息看,是在 ImageIO 解析图片信息的时候 Crash ,并且最后调用的方法都是看起来都是和 INameSpacePrefixMap 相关,推测 Crash 应该是和这个方法 CGImageSourceCopyPropertiesAtIndex 的实现有关。




  • 从堆栈信息看,这段代码是图片库在子线程通过 CGImageSourceCopyPropertiesAtIndex 解析 imageSource 中的图片相关信息,然后发生了野指针的 Crash。

  • CGImageSourceCopyPropertiesAtIndex 的输入只有一个 imageSourceimageSource 由图片的 data 生成,调用栈并没有多线程操作,可以排除是多线程操作 imageSource、data 导致的 Crash。

  • 看堆栈是在解析 PNG 图片,通过将下发的图片格式换成 JPG 格式,发现量级并没有降低。推测 Crash 不是某种特定图片格式引起的。

反汇编分析

反汇编准备

  • iOS 14.3 的 iPhone 8
  • ImageIO 系统库:~/Library/Developer/Xcode/iOS DeviceSupport目录下找到对应 iOS 14.3 的 ImageIO
  • 一份 iOS 14.3、iPhone 8 上发生的  CrashLog
  • Hopper

反汇编

1、从 CrashLog 上找到 Crash 对应的指令偏移地址 2555072


2、通过 Hopper 打开 ImageIO,跳转到指令偏移地址 2555072

Navigate => Go To File Offset 2555072


3、Crash 对应的指令应该是0000000181b09cc0 ldr x8, [x8, #0x10],可以看到应该是访问 [x8, #0x10]指向的内存出错


5、向上回溯查看 x8 的来源

  • 0000000181b09cbc ldr x8, [x20] x8 是存在 x20 指向的内存中(即 x8 = *x20

  • 0000000181b09c98 ldr x20, [x21, #0x8] x20 又存在[x21, #0x8] 指向的内存中

  • 0000000181b09c8c adrp x21, #0x1da0ed0000000000181b09c90 add x21, x21, #0xe10 x21 指向的是一个 data 段,推测 x21 应该是一个全局变量,所以,可能是这个全局变量野了,或者是这个全局变量引用的某些内存(x20)野了

6、运行时 debug 查看 x8、x20、x21 对应寄存器的值是什么

  • x21 从内存地址的名字看,应该是一个全局的 Map


8、经过在运行时反复调试,这个

AdobeXMPCore_Int::ManageDefaultNameSpacePrefixMap(bool) 会在多个方法中调用(并且调用时都加了锁,不太可能会出现 data race):

  • AdobeXMPCore_Int::INameSpacePrefixMap_I::CreateDefaultNameSpacePrefixMap()

  • AdobeXMPCore_Int::INameSpacePrefixMap_I::InsertInDefaultNameSpacePrefixMap(char const*, unsigned long long, char const*, unsigned long long)

  • AdobeXMPCore_Int::INameSpacePrefixMap_I::DestroyDefaultNameSapcePrefixMap()



9、在后台线程访问访问全局变量 sDefaultNameSpacePrefixMap 时 Crash,推测可能是用户手动杀进程后,全局变量在主线程已经被析构,后台线程还会继续访问这个全局变量,从而出现野指针访问异常。发现 Crash 日志的主线程堆栈也出现 _exit 的调用,可以确定是全局变量析构导致。


Crash 发生的原因:

在用户手动杀进程后,主线程将这个全局变量析构了,这时候子线程再访问这个全局变量就出现了野指针。


复现问题

尝试在子线程不断调用 CFDictionaryRef CGImageSourceCopyPropertiesAtIndex(CGImageSourceRef isrc, size_t index, CFDictionaryRef options);,并且手动杀掉进程触发这个 crash



可以证明上述的推理是正确的。

总结

  • CFDictionaryRef CGImageSourceCopyPropertiesAtIndex(CGImageSourceRef isrc, size_t index, CFDictionaryRef options); 这个方法在解析部分图片的时候最终会访问全局变量

收起阅读 »

Objective-C & Swift 最轻量级 Hook 方案-SDMagicHook

本文从一个 iOS 日常开发的 hook 案例入手,首先简要介绍了 Objective-C 的动态特性以及传统 hook 方式常见的命名冲突、操作繁琐、hook 链意外断裂、hook 作用范围不可控制等缺陷,然后详细介绍了一套基于消息转发机制的 instanc...
继续阅读 »

本文从一个 iOS 日常开发的 hook 案例入手,首先简要介绍了 Objective-C 的动态特性以及传统 hook 方式常见的命名冲突、操作繁琐、hook 链意外断裂、hook 作用范围不可控制等缺陷,然后详细介绍了一套基于消息转发机制的 instance 粒度的轻量级 hook 方案:SDMagicHook


背景

某年某月的某一天,产品小 S 向开发君小 Q 提出了一个简约而不简单的需求:扩大一下某个 button 的点击区域。小 Q 听完暗自窃喜:还好,这是一个我自定义的 button,只需要重写一下 button 的 pointInside:withEvent:方法即可。只见小 Q 手起刀落在产品小 S 崇拜的目光中轻松完成。代码如下:



次日,产品小 S 又一次满怀期待地找到开发君小 Q:欧巴~,帮我把这个 button 也扩大一下点击区域吧。小 Q 这次却犯了难,心中暗自思忖:这是系统提供的标准 UI 组件里面的 button 啊,我只能拿来用没法改呀,我看你这分明就是故意为难我胖虎!我…我…我.----小 Q 卒。

在这个 case 中,小 Q 的遭遇着实令人同情。但是痛定思痛,难道产品提出的这个问题真的无解吗?其实不然,各位看官静息安坐,且听我慢慢分析:


1. Objective-C 的动态特性

Objective-C 作为一门古老而又灵活的语言有很多动态特性为开发者所津津乐道,这其中尤其以动态类型(Dynamic typing)、动态绑定(Dynamic binding)、动态加载(Dynamic loading)等特性最为著名,许多在其他语言中看似不可能实现的功能也可以在 OC 中利用这些动态特性达到事半功倍的效果。

1.1 动态类型(Dynamic typing)

动态类型就是说运行时才确定对象的真正类型。例如我们可以向一个 id 类型的对象发送任何消息,这在编译期都是合法的,因为类型是可以动态确定的,消息真正起作用的时机也是在运行时这个对象的类型确定以后,这个下面就会讲到。我们甚至可以在运行时动态修改一个对象的 isa 指针从而修改其类型,OC 中 KVO 的实现正是对动态类型的典型应用。

1.2 动态绑定(Dynamic binding)

当一个对象的类型被确定后,其对应的属性和可响应的消息也被确定,这就是动态绑定。绑定完成之后就可以在运行时根据对象的类型在类型信息中查找真正的函数地址然后执行。

1.3 动态加载(Dynamic loading)

根据需求加载所需要的素材资源和代码资源,用户可根据需求加载一些可执行的代码资源,而不是在在启动的时候就加载所有的组件,可执行代码可以含有新的类。

了解了 OC 的这些动态特性之后,让我们再次回顾一下产品的需求要领:产品只想任性地修改任何一个 button 的点击区域,而恰巧这次这个 button 是系统原生组件中的一个子 View。所以当前要解决的关键问题就是如何去改变一个用系统原生类实例化出来的组件的“点击区域检测方法”。刚才在 OC 动态类型特性的介绍中我们说过“消息真正起作用的时机是在运行时这个对象的类型确定以后”、“我们甚至可以在运行时动态修改一个对象的 isa 指针从而修改其类型,OC 中 KVO 的实现正是对动态类型的典型应用”。看到这里,你应该大概有了一些思路,我们不妨照猫画虎模仿 KVO 的原理来实现一下。

2. 初版 SDMagicHook 方案

要想使用这种类似 KVO 的替换 isa 指针的方案,首先需要解决以下几个问题:

2.1 如何动态创建一个新的类

在 OC 中,我们可以调用 runtime 的 objc_allocateClassPairobjc_registerClassPair 函数动态地生成新的类,然后调用 object_setClass 函数去将某个对象的 isa 替换为我们自建的临时类。

2.2 如何给这些新建的临时类命名

作为一个有意义的临时类名,首先得可以直观地看出这个临时类与其基类的关系,所以我们可以这样拼接新的类名[NSString stringWithFormat:@“SDHook*%s”, originalClsName],但这有一个很明显的问题就是无法做到一个对象独享一个专有类,为此我们可以继续扩充下,不妨在类名中加上一个对象的唯一标记–内存地址,新的类名组成是这样的[NSString stringWithFormat:@“SDHook_%s_%p”, originalClsName, self],这次看起来似乎完美了,但在极端的情况下还会出问题,例如我们在一个一万次的 for 循环中不断创建同一种类型的对象,那么就会大概率出现新对象的内存地址和之前已经释放了的对象的内存地址一样,而我们会在一个对象析构后很快就会去释放它所使用的临时类,这就会有概率导致那个新生成的对象正在使用的类被释放了然后就发生了 crash。为解决此类问题,我们需要再在这个临时的类名中添加一个随机标记来降低这种情况发生的概率,最终的类名组成是这样的[NSString stringWithFormat:@“SDHook_%s_%p_%d”, originalClsName, self, mgr.randomFlag]


2.3 何时销毁这些临时类

我们通过 objc_setAssociatedObject 的方式可以为每个 NSObject 对象动态关联上一个 SDNewClassManager 实例,在 SDNewClassManager 实例里面持有当前对象所使用的临时类。当前对象销毁时也会销毁这个 SDNewClassManager 实例,然后我们就可以在 SDNewClassManager 实例的 dealloc 方法里面做一些销毁临时类的操作。但这里我们又不能立即做销毁临时类的操作,因为此时这个对象还没有完全析构,它还在做一些其它善后操作,如果此时去销毁那个临时类必然会造成 crash,所以我们需要稍微延迟一段时间来做这些临时类的销毁操作,代码如下:




好了,到目前为止我们已经实现了第一版 hook 方案,不过这里两个明显的问题:

  1. 每次 hook 都要增加一个 category 定义一个函数相对比较麻烦;
  2. 如果我们在某个 Class 的两个 category 里面分别实现了一个同名的方法就会导致只有一个方法最终能被调用到。

为此,我们研发了第二版针对第一版的不足予以改进和优化。

3. 优化版 SDMagicHook 方案

针对上面提到的两个问题,我们可以通过用 block 生成 IMP 然后将这个 IMP 替换到目标 Selector 对应的 method 上即可,API 示例代码如下:


这个 block 方案看上去确实简洁和方便了很多,但同样面临着任何一个 hook 方案都避不开的问题那就是,如何在 block 里面调用原生的对应方法呢?

3.1 关键点一:如何在 block 里面调用原生方法

在初版方案中,我们在一个类的 category 中增加了一个 hook 专用的方法,然后在完成方法交换之后通过向实例发送 hook 专用的方法自身对应的 selector 消息即可实现对原生方法的回调。但是现在我们是使用的 block 创建了一个“匿名函数”来替换原生方法,既然是匿名函数也就没有明确的 selector,这也就意味着我们根本没有办法在方法交换后找到它的原生方法了!

那么眼下的关键问题就是找到一个合适的 Selector 来映射到被 hook 的原生函数。而目前来看,我们唯一可以在当前编译环境下方便调用且和这个 block 还有一定关联关系的 Selector 就是原方法的 Selector 也就是我们的 demo 中的pointInside:withEvent:了。这样一来pointInside:withEvent:这个 Selector 就变成了一个一对多的映射 key,当有人在外部向我们的 button 发送 pointInside:withEvent:消息时,我们应该首先将 pointInside:withEvent:转发给我们自定义的 block 实现的 IMP,然后当在 block 内部再次向 button 发送 pointInside:withEvent:消息时就将这个消息转发给系统原生的方法实现,如此一来就可以完成了一次完美的方法调度了。

3.2 关键点二:如何设计消息调度方案

在 OC 中要想调度方法派发就需要拿到消息转发的控制权,而要想获得这个消息转发控制权就需要强制让这个 receiver 每次收到这个消息都触发其消息转发机制然后我们在消息转发的过程中做对应的调度。在这个例子中我们将目标 button 的 pointInside:withEvent:对应的 method 的 imp 指针替换为_objc_msgForward,这样每当有人调用这个 button 的 pointInside:withEvent:方法时最终都会走到消息转发方法 forwardInvocation:里面,我们实现这个方法来完成具体的方法调度工作。

因为目标 button 的 pointInside:withEvent:对应的 method 的 imp 指针被替换成了_objc_msgForward,所以我们需要另外新增一个方法 A 和方法 B 来分别存储目标 button 的 pointInside:withEvent:方法的 block 自定义实现和原生实现。然后当需要在自定义的方法内部调用原始方法时通过调用 callOriginalMethodInBlock:这个 api 来显式告知,示例代码如下:



当目标 button 实例收到 pointInside:withEvent:消息时会启用我们自定义的消息调度机制,检查如果 OriginalCallFlag 为 false 就去调用自定义实现方法 A,否则就去调用原始实现方法 B,从而顺利实现一次方法调度。流程图及示例代码如下:




想象这样一个应用场景:有一个全局的 keywindow,各个业务都想监听一下 keywindow 的 layoutSubviews 方法,那我们该如何去管理和维护添加到 keywindow 上的多个 hook 实现之间的关系呢?如果一个对象要销毁了,它需要移除掉之前对 keywindow 的 hook,这时又该如何处理呢?

我们的解决方案是为每个被 hook 的目标原生方法生成一张 hook 表,按照 hook 发生的顺序依次为其生成内部 selector 并加入到 hook 表中。当 keywindow 收到 layoutSubviews 消息时,我们从 hook 表中取出该次消息对应的 hook selector 发送给 keywindow 让它执行对应的动作。如果删除某个 hook 也只需将其对应的 selector 从 hook 表中移除即可。代码如下:




4. 防止 hook 链意外断裂

我们都知道在对某个方法进行 hook 操作时都需要在我们的 hook 代码方法体中调用一下被 hook 的那个原始方法,如果遗漏了此步操作就会造成 hook 链断裂,这样就会导致被 hook 的那个原始方法永远不会被调用到,如果有人在你之前也 hook 了这个方法的话就会导致在你之前的所有 hook 都莫名失效了,因为这是一个很隐蔽的问题所以你往往很难意识到你的 hook 操作已经给其他人造成了严重的问题。

为了方便 hook 操作者快速及时发现这一问题,我们在 DEBUG 模式下增加了一套“hook 链断裂检测机制”,其实现原理大致如下:

前面已经提到过,我们实现了对 hook 目标方法的自定义调度,这就使得我们有机会在这些方法调用结束后检测其是否在方法执行过程中通过 callOriginalMethodInBlock 调用原始方法。如果发现某个方法体不是被 hook 的目标函数的最原始的方法体且这次方法执行结束之后也没有调用过原始方法就会通过 raise(SIGTRAP)方式发送一个中断信号暂停当前的程序以提醒开发者当次 hook 操作没有调用原始方法。



5. SDMagicHook 的优缺点

与传统的在 category 中新增一个自定义方法然后进行 hook 的方案对比,SDMagicHook 的优缺点如下:

优点:

  1. 只用一个 block 即可对任意一个实例的任意方法实现 hook 操作,不需要新增任何 category,简洁高效,可以大大提高你调试程序的效率;
  2. hook 的作用域可以控制在单个实例粒度内,将 hook 的副作用降到最低;
  3. 可以对任意普通实例甚至任意类进行 hook 操作,无论这个实例或者类是你自己生成的还是第三方提供的;
  4. 可以随时添加或去除者任意 hook,易于对 hook 进行管理。

缺点:

  1. 为了保证增删 hook 时的线程安全,SDMagicHook 进行增删 hook 相关的操作时在实例粒度内增加了读写锁,如果有在多线程频繁的 hook 操作可能会带来一点线程等待开销,但是大多数情况下可以忽略不计;
  2. 因为是基于实例维度的所以比较适合处理对某个类的个别实例进行 hook 的场景,如果你需要你的 hook 对某个类的所有实例都生效建议继续沿用传统方式的 hook。

总结

SDMagicHook 方案在 OC 中和 Swift 的 UIKit 层均可直接使用,而且 hook 作用域可以限制在你指定的某个实例范围内从而避免污染其它不相关的实例。Api 设计简洁易用,你只需要花费一分钟的时间即可轻松快速上手,希望我们的这套方案可以给你带来更美妙的 iOS 开发体验。



Github 项目地址:https://github.com/larksuite/SDMagicHook

源码下载:SDMagicHook-master.zip



收起阅读 »

iOS基于二进制文件重排的解决方案 APP启动速度提升超15%!

背景启动是App给用户的第一印象,对用户体验至关重要。业务迭代迅速,如果放任不管,启动速度会一点点劣化。为此iOS客户端团队做了大量优化工作,除了传统的修改业务代码方式,我们还做了些开拓性的探索,发现修改代码在二进制文件的布局可以提高启动性能,方案落地后在上启...
继续阅读 »

背景

启动是App给用户的第一印象,对用户体验至关重要。业务迭代迅速,如果放任不管,启动速度会一点点劣化。为此iOS客户端团队做了大量优化工作,除了传统的修改业务代码方式,我们还做了些开拓性的探索,发现修改代码在二进制文件的布局可以提高启动性能,方案落地后在上启动速度提高了约15%。

本文从原理出发,介绍了我们是如何通过静态扫描和运行时trace找到启动时候调用的函数,然后修改编译参数完成二进制文件的重新排布。

原理

Page Fault

进程如果能直接访问物理内存无疑是很不安全的,所以操作系统在物理内存的上又建立了一层虚拟内存。为了提高效率和方便管理,又对虚拟内存和物理内存又进行分页(Page)。当进程访问一个虚拟内存Page而对应的物理内存却不存在时,会触发一次缺页中断(Page Fault),分配物理内存,有需要的话会从磁盘mmap读人数据。

通过App Store渠道分发的App,Page Fault还会进行签名验证,所以一次Page Fault的耗时比想象的要多:



Page Fault

重排

编译器在生成二进制代码的时候,默认按照链接的Object File(.o)顺序写文件,按照Object File内部的函数顺序写函数。

静态库文件.a就是一组.o文件的ar包,可以用ar -t查看.a包含的所有.o。




默认布局

简化问题:假设我们只有两个page:page1/page2,其中绿色的method1和method3启动时候需要调用,为了执行对应的代码,系统必须进行两个Page Fault。

但如果我们把method1和method3排布到一起,那么只需要一个Page Fault即可,这就是二进制文件重排的核心原理。





重排之后

我们的经验是优化一个Page Fault,启动速度提升0.6~0.8ms。

核心问题

为了完成重排,有以下几个问题要解决:

  • 重排效果怎么样 - 获取启动阶段的page fault次数

  • 重排成功了没 - 拿到当前二进制的函数布局

  • 如何重排 - 让链接器按照指定顺序生成Mach-O

  • 重排的内容 - 获取启动时候用到的函数

System Trace

日常开发中性能分析是用最多的工具无疑是Time Profiler,但Time Profiler是基于采样的,并且只能统计线程实际在运行的时间,而发生Page Fault的时候线程是被blocked,所以我们需要用一个不常用但功能却很强大的工具:System Trace。

选中主线程,在VM Activity中的File Backed Page In次数就是Page Fault次数,并且双击还能按时序看到引起Page Fault的堆栈:







System Trace

signpost

现在我们在Instrument中已经能拿到某个时间段的Page In次数,那么如何和启动映射起来呢?

我们的答案是:os_signpost

os_signpost是iOS 12开始引入的一组API,可以在Instruments绘制一个时间段,代码也很简单:


1os_log_t logger = os_log_create("com.bytedance.tiktok", "performance");2os_signpost_id_t signPostId = os_signpost_id_make_with_pointer(logger,sign);3//标记时间段开始4os_signpost_interval_begin(logger, signPostId, "Launch","%{public}s", "");5//标记结束6os_signpost_interval_end(logger, signPostId, "Launch");

通常可以把启动分为四个阶段处理:



启动阶段

有多少个Mach-O,就会有多少个Load和C++静态初始化阶段,用signpost相关API对对应阶段打点,方便跟踪每个阶段的优化效果。

Linkmap

Linkmap是iOS编译过程的中间产物,记录了二进制文件的布局,需要在Xcode的Build Settings里开启Write Link Map File:


Build Settings

比如以下是一个单页面Demo项目的linkmap。



linkmap

linkmap主要包括三大部分:

  • Object Files 生成二进制用到的link单元的路径和文件编号

  • Sections 记录Mach-O每个Segment/section的地址范围

  • Symbols 按顺序记录每个符号的地址范围

ld

Xcode使用的链接器件是ld,ld有一个不常用的参数-order_file,通过man ld可以看到详细文档:

Alters the order in which functions and data are laid out. For each section in the output file, any symbol in that section that are specified in the order file file is moved to the start of its section and laid out in the same order as in the order file file.

可以看到,order_file中的符号会按照顺序排列在对应section的开始,完美的满足了我们的需求。

Xcode的GUI也提供了order_file选项:




order_file

如果order_file中的符号实际不存在会怎么样呢?

ld会忽略这些符号,如果提供了link选项-order_file_statistics,会以warning的形式把这些没找到的符号打印在日志里。

获得符号

还剩下最后一个,也是最核心的一个问题,获取启动时候用到的函数符号。

我们首先排除了解析Instruments(Time Profiler/System Trace) trace文件方案,因为他们都是基于特定场景采样的,大多数符号获取不到。最后选择了静态扫描+运行时Trace结合的解决方案。

Load

Objective C的符号名是+-[Class_name(category_name) method:name:],其中+表示类方法,-表示实例方法。

刚刚提到linkmap里记录了所有的符号名,所以只要扫一遍linkmap的__TEXT,__text,正则匹配("^\+\[.*\ load\]$")既可以拿到所有的load方法符号。

C++静态初始化

C++并不像Objective C方法那样,大部分方法调用编译后都是objc_msgSend,也就没有一个入口函数去运行时hook。

但是可以用-finstrument-functions在编译期插桩“hook”,但由于APP很多依赖由其他团队提供静态库,这套方案需要修改依赖的构建过程。二进制文件重排在没有业界经验可供参考,不确定收益的情况下,选择了并不完美但成本最低的静态扫描方案。

1. 扫描linkmap的__DATA,__mod_init_func,这个section存储了包含C++静态初始化方法的文件,获得文件号[ 5]


1//__mod_init_func20x100008060    0x00000008  [  5] ltmp73//[  5]对应的文件4[  5] .../Build/Products/Debug-iphoneos/libStaticLibrary.a(StaticLibrary.o)

2. 通过文件号,解压出.o。

1➜  lipo libStaticLibrary.a -thin arm64 -output arm64.a2ar -x arm64.a StaticLibrary.o

3. 通过.o,获得静态初始化的符号名_demo_constructor

1  objdump -r -section=__mod_init_func StaticLibrary.o23StaticLibrary.o:    file format Mach-O arm6445RELOCATION RECORDS FOR [__mod_init_func]:60000000000000000 ARM64_RELOC_UNSIGNED _demo_constructor

4. 通过符号名,文件号,在linkmap中找到符号在二进制中的范围:

10x100004A30    0x0000001C  [  5] _demo_constructor

5. 通过起始地址,对代码进行反汇编:

 1  objdump -d --start-address=0x100004A30 --stop-address=0x100004A4B demo_arm64  2 3_demo_constructor: 4100004a30:    fd 7b bf a9     stp x29, x30, [sp, #-16]! 5100004a34:    fd 03 00 91     mov x29, sp 6100004a38:    20 0c 80 52     mov w0, #97 7100004a3c:    da 06 00 94     bl  #7016  8100004a40:    40 0c 80 52     mov w0, #98 9100004a44:    fd 7b c1 a8     ldp x29, x30, [sp], #1610100004a48:    d7 06 00 14     b   #7004 

6. 通过扫描bl指令扫描子程序调用,子程序在二进制的开始地址为:100004a3c +1b68(对应十进制的7016)。

1100004a3c:    da 06 00 94     bl  #7016 

7. 通过开始地址,可以找到符号名和结束地址,然后重复5~7,递归的找到所有的子程序调用的函数符号。


小坑

STL里会针对string生成初始化函数,这样会导致多个.o里存在同名的符号,例如:

1__ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEC1IDnEEPKc

类似这样的重复符号的情况在C++里有很多,所以C/C++符号在order_file里要带着所在的.o信息:

1//order_file.txt2libDemoLibrary.a(object.o):__GLOBAL__sub_I_demo_file.cpp

局限性

branch系列汇编指令除了bl/b,还有br/blr,即通过寄存器的间接子程序调用,静态扫描无法覆盖到这种情况。

Local符号

在做C++静态初始化扫描的时候,发现扫描出了很多类似l002的符号。经过一番调研,发现是依赖方输出静态库的时候裁剪了local符号。导致__GLOBAL__sub_I_demo_file.cpp 变成了l002。

需要静态库出包的时候保留local符号,CI脚本不要执行strip -x,同时Xcode对应target的Strip Style修改为Debugging symbol:








Strip Style

静态库保留的local符号会在宿主App生成IPA之前裁剪掉,所以不会对最后的IPA包大小有影响。宿主App的Strip Style要选择All Symbols,宿主动态库选择Non-Global Symbols。

Objective C方法

绝大部分Objective C的方法在编译后会走objc_msgSend,所以通过fishhook(https://github.com/facebook/fishhook) hook这一个C函数即可获得Objective C符号。由于objc_msgSend是变长参数,所以hook代码需要用汇编来实现:


 1//代码参考InspectiveC 2__attribute__((__naked__)) 3static void hook_Objc_msgSend() { 4    save() 5    __asm volatile ("mov x2, lr\n"); 6    __asm volatile ("mov x3, x4\n"); 7    call(blr, &before_objc_msgSend) 8    load() 9    call(blr, orig_objc_msgSend)10    save()11    call(blr, &after_objc_msgSend)12    __asm volatile ("mov lr, x0\n");13    load()14    ret()15}


子程序调用时候要保存和恢复参数寄存器,所以save和load分别对x0~x9, q0~q9入栈/出栈。call则通过寄存器来间接调用函数:


 1#define save() \ 2__asm volatile ( \ 3"stp q6, q7, [sp, #-32]!\n"\ 4... 5 6#define load() \ 7__asm volatile ( \ 8"ldp x0, x1, [sp], #16\n" \ 9...1011#define call(b, value) \12__asm volatile ("stp x8, x9, [sp, #-16]!\n"); \13__asm volatile ("mov x12, %0\n" :: "r"(value)); \14__asm volatile ("ldp x8, x9, [sp], #16\n"); \15__asm volatile (#b " x12\n");


before_objc_msgSend中用栈保存lr,在after_objc_msgSend恢复lr。由于要生成trace文件,为了降低文件的大小,直接写入的是函数地址,且只有当前可执行文件的Mach-O(app和动态库)代码段才会写入:

iOS中,由于ALSR(https://en.wikipedia.org/wiki/Address_space_layout_randomization)的存在,在写入之前需要先减去偏移量slide:

1IMP imp = (IMP)class_getMethodImplementation(object_getClass(self), _cmd);2unsigned long imppos = (unsigned long)imp;3unsigned long addr = immpos - macho_slide

获取一个二进制的__text段地址范围:

1unsigned long size = 0;2unsigned long start = (unsigned long)getsectiondata(mhp,  "__TEXT", "__text", &size);3unsigned long end = start + size;

获取到函数地址后,反查linkmap既可找到方法的符号名。

Block

block是一种特殊的单元,block在编译后的函数体是一个C函数,在调用的时候直接通过指针调用,并不走objc_msgSend,所以需要单独hook。

通过Block的源码可以看到block的内存布局如下:

 1struct Block_layout { 2    void *isa; 3    int32_t flags; // contains ref count 4    int32_t reserved; 5    void  *invoke; 6    struct Block_descriptor1 *descriptor; 7}; 8struct Block_descriptor1 { 9    uintptr_t reserved;10    uintptr_t size;11};
其中invoke就是函数的指针,hook思路是将invoke替换为自定义实现,然后在reserved保存为原始实现。

1//参考 https://github.com/youngsoft/YSBlockHook2if (layout->descriptor != NULL && layout->descriptor->reserved == NULL)3{4    if (layout->invoke != (void *)hook_block_envoke)5    {6        layout->descriptor->reserved = layout->invoke;7        layout->invoke = (void *)hook_block_envoke;8    }9}

由于block对应的函数签名不一样,所以这里仍然采用汇编来实现hook_block_envoke

 1__attribute__((__naked__)) 2static void hook_block_envoke() { 3    save() 4    __asm volatile ("mov x1, lr\n"); 5    call(blr, &before_block_hook); 6    __asm volatile ("mov lr, x0\n"); 7    load() 8    //调用原始的invoke,即resvered存储的地址 9    __asm volatile ("ldr x12, [x0, #24]\n");10    __asm volatile ("ldr x12, [x12]\n");11    __asm volatile ("br x12\n");12}

before_block_hook中获得函数地址(同样要减去slide)。

1intptr_t before_block_hook(id block,intptr_t lr)2{3    Block_layout * layout = (Block_layout *)block;4    //layout->descriptor->reserved即block的函数地址5    return lr;6}

同样,通过函数地址反查linkmap既可找到block符号。


瓶颈

基于静态扫描+运行时trace的方案仍然存在少量瓶颈:

  • initialize hook不到

  • 部分block hook不到

  • C++通过寄存器的间接函数调用静态扫描不出来

目前的重排方案能够覆盖到80%~90%的符号,未来我们会尝试编译期插桩等方案来进行100%的符号覆盖,让重排达到最优效果。

整体流程

流程

  1. 设置条件触发流程

  2. 工程注入Trace动态库,选择release模式编译出.app/linkmap/中间产物

  3. 运行一次App到启动结束,Trace动态库会在沙盒生成Trace log

  4. 以Trace Log,中间产物和linkmap作为输入,运行脚本解析出order_file

总结

目前,在缺少业界经验参考的情况下,我们成功验证了二进制文件重排方案在iOS APP开发中的可行性和稳定性。基于二进制文件重排,我们在针对iOS客户端上的优化工作中,获得了约15%的启动速度提升。

抽象来看,APP开发中大家会遇到这样一个通用的问题,即在某些情况下,APP运行需要进行大量的Page Fault,这会影响代码执行速度。而二进制文件重排方案,目前看来是解决这一通用问题比较好的方案。


转载于字节跳动技术团队:https://mp.weixin.qq.com/s/Drmmx5JtjG3UtTFksL6Q8Q








收起阅读 »

WKWebView音视频媒体播放处理

1. 对WKWebViewConfiguration进行设置。实现媒体文件可以自动播放、使用内嵌HTML5播放等功能使用这个测试网址// 初始化配置对象 WKWebViewConfiguration *configuration = [[WKWebViewCo...
继续阅读 »

1. 对WKWebViewConfiguration进行设置。

实现媒体文件可以自动播放、使用内嵌HTML5播放等功能
使用这个测试网址

// 初始化配置对象
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
// 默认是NO,这个值决定了用内嵌HTML5播放视频还是用本地的全屏控制
configuration.allowsInlineMediaPlayback = YES;
// 自动播放, 不需要用户采取任何手势开启播放
// WKAudiovisualMediaTypeNone 音视频的播放不需要用户手势触发, 即为自动播放
configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone;
configuration.allowsAirPlayForMediaPlayback = YES;
configuration.allowsPictureInPictureMediaPlayback = YES;

self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:configuration];
self.webView.navigationDelegate = self;
NSURL *url =[NSURL URLWithString:@"测试网址"];
[self.webView loadRequest:[NSURLRequest requestWithURL:url]];
[self.view addSubview:self.webView];

由于H5的video未设置autoplay、playsinline属性。我们需自己注入,才能实现效果。

NSString *jSString = @"document.getElementsByTagName('video')[0].setAttribute('playsinline','');";
NSString *jSString2 = @"document.getElementsByTagName('video')[0].autoplay=true;";
//用于进行JavaScript注入
WKUserScript *wkUScript = [[WKUserScript alloc] initWithSource:jSString injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
WKUserScript *wkUScript2 = [[WKUserScript alloc] initWithSource:jSString2 injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
[configuration.userContentController addUserScript:wkUScript];
[configuration.userContentController addUserScript:wkUScript2];


2. 监听网页内播放器的回调

可以使用两种办法。

2.1 利用HTML5 Audio/Video 事件

HTML5 Audio/Video 事件代码可以由H5同事完成,也可以由App端注入。
注入代码如下:


NSString *jSString3 = @"document.getElementsByTagName('video')[0].addEventListener('canplay', function(e) {window.webkit.messageHandlers.readytoplay.postMessage(\"canplay\");})";
NSString *jSString4 = @"document.getElementsByTagName('video')[0].addEventListener('pause', function(e) {window.webkit.messageHandlers.pause.postMessage(\"pause\");})";
NSString *jSString5 = @"document.getElementsByTagName('video')[0].addEventListener('play', function(e) {window.webkit.messageHandlers.play.postMessage(\"play\");})";
NSString *jSString6 = @"document.getElementsByTagName('video')[0].addEventListener('ended', function(e) {window.webkit.messageHandlers.ended.postMessage(\"ended\");})";
WKUserScript *wkUScript3 = [[WKUserScript alloc] initWithSource:jSString3 injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
[configuration.userContentController addUserScript:wkUScript3];
WKUserScript *wkUScript4 = [[WKUserScript alloc] initWithSource:jSString4 injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
[configuration.userContentController addUserScript:wkUScript4];
WKUserScript *wkUScript5 = [[WKUserScript alloc] initWithSource:jSString5 injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
[configuration.userContentController addUserScript:wkUScript5];
WKUserScript *wkUScript6 = [[WKUserScript alloc] initWithSource:jSString6 injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
[configuration.userContentController addUserScript:wkUScript6];
App端接收js的代码如下:
需遵守WKScriptMessageHandler协议

@interface ViewController () <WKNavigationDelegate,WKScriptMessageHandler>
@end

再为WKWebViewConfiguration添加协议

//添加一个协议
[configuration.userContentController addScriptMessageHandler:self name:@"readytoplay"];
[configuration.userContentController addScriptMessageHandler:self name:@"play"];
[configuration.userContentController addScriptMessageHandler:self name:@"pause"];
[configuration.userContentController addScriptMessageHandler:self name:@"ended"];

使用以下方法即可获取播放器事件

#pragma mark - WKScriptMessageHandler

//! WKWebView收到ScriptMessage时回调此方法
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
if ([message.name caseInsensitiveCompare:@"readytoplay"] == NSOrderedSame) {
NSLog(@"video is readytoplay");
}
if ([message.name caseInsensitiveCompare:@"play"] == NSOrderedSame) {
NSLog(@"video is play");
}
if ([message.name caseInsensitiveCompare:@"pause"] == NSOrderedSame) {
NSLog(@"video is pause");
}
if ([message.name caseInsensitiveCompare:@"ended"] == NSOrderedSame) {
NSLog(@"video is ended");
}
}
2.2 还有一种是App可自己实现的,使用AVAudioSession进行监听:

使用AVAudioSession监听,必须用到AVAudioSessionCategoryOptionMixWithOthers。这样会导致切换别的音视频App不会打断播放器。例如网易云音乐、bilibili。
手机来电会打断播放器。


NSError *sessionError = nil;
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback
withOptions:AVAudioSessionCategoryOptionMixWithOthers
error:&sessionError];
[[AVAudioSession sharedInstance] setActive:YES error:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(audioSessionSilenceSecondaryAudioHint:)
name:AVAudioSessionSilenceSecondaryAudioHintNotification
object:[AVAudioSession sharedInstance]];
- (void)audioSessionSilenceSecondaryAudioHint:(NSNotification *)notification
{
NSDictionary *userInfo = notification.userInfo;
NSLog(@"audioSessionSilenceSecondaryAudioHint %@",userInfo);
}

开始播放输出:

2021-04-01 15:22:31.302248+0800 webViewPlayMedia[18078:2811391] audioSessionSilenceSecondaryAudioHint  {
AVAudioSessionSilenceSecondaryAudioHintTypeKey = 1;

结束播放输出:

2021-04-01 15:22:31.382646+0800 webViewPlayMedia[18078:2811391] audioSessionSilenceSecondaryAudioHint  {
AVAudioSessionSilenceSecondaryAudioHintTypeKey = 0;

3. 获取视频播放地址,使用自定义播放器进行播放

- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation {
NSLog(@"WKPhoneWebView didFinishNavigation");

NSString *JsStr = @"(document.getElementsByTagName(\"video\")[0]).src";
[self.webView evaluateJavaScript:JsStr completionHandler:^(id _Nullable response, NSError * _Nullable error) {
if(![response isEqual:[NSNull null]] && response != nil){
//截获到视频地址了
NSLog(@"response == %@",response);
}else{
//没有视频链接
}
}];
}

4. 坑

4.1 播放视频,会有ERROR提示:
2021-04-01 09:34:57.361477+0800 webViewPlayMedia[17109:2655981] [assertion] Error acquiring assertion: <Error Domain=RBSAssertionErrorDomain Code=3 "Required client entitlement is missing" UserInfo={RBSAssertionAttribute=<RBSDomainAttribute| domain:"com.apple.webkit" name:"MediaPlayback" sourceEnvironment:"(null)">, NSLocalizedFailureReason=Required client entitlement is missing}>
2021-04-01 09:34:57.361610+0800 webViewPlayMedia[17109:2655981] [ProcessSuspension] 0x1043dc990 - ProcessAssertion: Failed to acquire RBS MediaPlayback assertion 'WebKit Media Playback' for process with PID 17110, error: Error Domain=RBSAssertionErrorDomain Code=3 "Required client entitlement is missing" UserInfo={RBSAssertionAttribute=<RBSDomainAttribute| domain:"com.apple.webkit" name:"MediaPlayback" sourceEnvironment:"(null)">, NSLocalizedFailureReason=Required client entitlement is missing}


但是设置了background属性了,依然无法解除,但是不影响播放。
这个问题在https://stackoverflow.com/questions/66493177/required-client-entitlement-is-missing-in-wkwebview亦有提出,但是没有解决方案。

4.2 iOS13.2 13.3系统手机会在加载WKWebView时会连续报错:
2021-04-01 15:55:11.083253+0800 webViewPlayMedia[342:59346] [Process] kill() returned unexpected error 1

在该系统版本下,WKWebView使用配置WKWebViewConfiguration,会无法播放。

资料:收到控制台警告:当我在iOS13.2中加载WKWebView时,[Process] kill() returned unexpected error 1
该错误已在13.4版本中修复。



作者:左方
链接:https://www.jianshu.com/p/a77a33063755


收起阅读 »

iOS抖音的转场动画

转场调用代码- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { AwemeListV...
继续阅读 »



转场调用代码


- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
AwemeListViewController *awemeVC = [[AwemeListViewController alloc] init];
awemeVC.transitioningDelegate = self; //0

// 1
UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
// 2
CGRect cellFrame = cell.frame;
// 3
CGRect cellConvertedFrame = [collectionView convertRect:cellFrame toView:collectionView.superview];

//弹窗转场
self.presentScaleAnimation.cellConvertFrame = cellConvertedFrame; //4

//消失转场
self.dismissScaleAnimation.selectCell = cell; // 5
self.dismissScaleAnimation.originCellFrame = cellFrame; //6
self.dismissScaleAnimation.finalCellFrame = cellConvertedFrame; //7

awemeVC.modalPresentationStyle = UIModalPresentationOverCurrentContext; //8
self.modalPresentationStyle = UIModalPresentationCurrentContext; //9

[self.leftDragInteractiveTransition wireToViewController:awemeVC];
[self presentViewController:awemeVC animated:YES completion:nil];
}

0 处代码使我们需要把当前的类做为转场的代理
1 这里我们要拿出cell这个view
2 拿出当前Cell的frame坐标
3 cell的坐标转成屏幕坐标
4 设置弹出时候需要cell在屏幕的位置坐标
5 设置消失转场需要的选中cell视图
6 设置消失转场原始cell坐标位置
7 设置消失转场最终得cell屏幕坐标位置 用于消失完成回到原来位置的动画
8 设置弹出得vc弹出样式 这个用于显示弹出VC得时候 默认底部使blua的高斯模糊
9 设置当前VC的模态弹出样式为当前的弹出上下文

5~7 步设置的消失转场动画 下面会讲解
这里我们用的是前面讲上下滑的VC对象 大家不必担心 当它是一个普通的UIViewController即可

实现转场所需要的代理

首先在需要实现UIViewControllerTransitioningDelegate这个代理


 #pragma mark -
#pragma mark - UIViewControllerAnimatedTransitioning Delegate
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {

return self.presentScaleAnimation; //present VC
}

- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
return self.dismissScaleAnimation; //dismiss VC
}

- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator {
return self.leftDragInteractiveTransition.isInteracting? self.leftDragInteractiveTransition: nil;
}

这里面我们看到我们分别返回了

  • 弹出动画实例self.presentScaleAnimation
  • dismiss动画实例self.dismissScaleAnimation
  • 以及self.leftDragInteractiveTransition实例用于负责转场切换的具体实现

所以我们需要在 当前的VC中声明3个成员变量 并初始化

@property (nonatomic, strong) PresentScaleAnimation *presentScaleAnimation;
@property (nonatomic, strong) DismissScaleAnimation *dismissScaleAnimation;
@property (nonatomic, strong) DragLeftInteractiveTransition *leftDragInteractiveTransition;

并在viewDidLoad:方法中初始化一下

 //转场的两个动画
self.presentScaleAnimation = [[PresentScaleAnimation alloc] init];
self.dismissScaleAnimation = [[DismissScaleAnimation alloc] init];
self.leftDragInteractiveTransition = [DragLeftInteractiveTransition new];

这里我说一下这三个成员都负责啥事
首先DragLeftInteractiveTransition类负责转场的 手势 过程,就是pan手势在这个类里面实现,并继承自UIPercentDrivenInteractiveTransition类,这是iOS7以后系统提供的转场基类必须在interactionControllerForDismissal:代理协议中返回这个类或者子类的实例对象,所以我们生成一个成员变量self.leftDragInteractiveTransition

其次是弹出present和消失dismiss的动画类,这俩类其实是负责简单的手势完成之后的动画.

这两个类都是继承自NSObject并实现UIViewControllerAnimatedTransitioning协议的类,这个协议里面有 需要你复写某些方法返回具体的动画执行时间,和中间过程中我们需要的相关的容器视图以及控制器的视图实例,当我们自己执行完成之后调用相关的block回答告知转场是否完成就行了.

 @implementation PresentScaleAnimation

- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext{
return 0.3f;
}

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
if (CGRectEqualToRect(self.cellConvertFrame, CGRectZero)) {
[transitionContext completeTransition:YES];
return;
}
CGRect initialFrame = self.cellConvertFrame;

UIView *containerView = [transitionContext containerView];
[containerView addSubview:toVC.view];

CGRect finalFrame = [transitionContext finalFrameForViewController:toVC];
NSTimeInterval duration = [self transitionDuration:transitionContext];

toVC.view.center = CGPointMake(initialFrame.origin.x + initialFrame.size.width/2, initialFrame.origin.y + initialFrame.size.height/2);
toVC.view.transform = CGAffineTransformMakeScale(initialFrame.size.width/finalFrame.size.width, initialFrame.size.height/finalFrame.size.height);

[UIView animateWithDuration:duration
delay:0
usingSpringWithDamping:0.8
initialSpringVelocity:1
options:UIViewAnimationOptionLayoutSubviews
animations:^{
toVC.view.center = CGPointMake(finalFrame.origin.x + finalFrame.size.width/2, finalFrame.origin.y + finalFrame.size.height/2);
toVC.view.transform = CGAffineTransformMakeScale(1, 1);
} completion:^(BOOL finished) {
[transitionContext completeTransition:YES];
}];
}
@end

很简单.

消失的动画 同上边差不多

@interface DismissScaleAnimation ()

@end

@implementation DismissScaleAnimation

- (instancetype)init {
self = [super init];
if (self) {
_centerFrame = CGRectMake((ScreenWidth - 5)/2, (ScreenHeight - 5)/2, 5, 5);
}
return self;
}

- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext{
return 0.25f;
}

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
// UINavigationController *toNavigation = (UINavigationController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
// UIViewController *toVC = [toNavigation viewControllers].firstObject;


UIView *snapshotView;
CGFloat scaleRatio;
CGRect finalFrame = self.finalCellFrame;
if(self.selectCell && !CGRectEqualToRect(finalFrame, CGRectZero)) {
snapshotView = [self.selectCell snapshotViewAfterScreenUpdates:NO];
scaleRatio = fromVC.view.frame.size.width/self.selectCell.frame.size.width;
snapshotView.layer.zPosition = 20;
}else {
snapshotView = [fromVC.view snapshotViewAfterScreenUpdates:NO];
scaleRatio = fromVC.view.frame.size.width/ScreenWidth;
finalFrame = _centerFrame;
}

UIView *containerView = [transitionContext containerView];
[containerView addSubview:snapshotView];

NSTimeInterval duration = [self transitionDuration:transitionContext];

fromVC.view.alpha = 0.0f;
snapshotView.center = fromVC.view.center;
snapshotView.transform = CGAffineTransformMakeScale(scaleRatio, scaleRatio);
[UIView animateWithDuration:duration
delay:0
usingSpringWithDamping:0.8
initialSpringVelocity:0.2
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
snapshotView.transform = CGAffineTransformMakeScale(1.0f, 1.0f);
snapshotView.frame = finalFrame;
} completion:^(BOOL finished) {
[transitionContext finishInteractiveTransition];
[transitionContext completeTransition:YES];
[snapshotView removeFromSuperview];
}];
}



@end
我们重点需要说一下 转场过渡的类DragLeftInteractiveTransition继承自UIPercentDrivenInteractiveTransition负责转场过程,
头文件的声明

@interface DragLeftInteractiveTransition : UIPercentDrivenInteractiveTransition

/** 是否正在拖动返回 标识是否正在使用转场的交互中 */
@property (nonatomic, assign) BOOL isInteracting;


/**
设置需要返回的VC

@param viewController 控制器实例
*/

-(void)wireToViewController:(UIViewController *)viewController;


@end


实现


@interface DragLeftInteractiveTransition ()

@property (nonatomic, strong) UIViewController *presentingVC;
@property (nonatomic, assign) CGPoint viewControllerCenter;
@property (nonatomic, strong) CALayer *transitionMaskLayer;

@end

@implementation DragLeftInteractiveTransition

#pragma mark -
#pragma mark - override methods 复写方法
-(CGFloat)completionSpeed{
return 1 - self.percentComplete;
}

- (void)updateInteractiveTransition:(CGFloat)percentComplete {
NSLog(@"%.2f",percentComplete);

}

- (void)cancelInteractiveTransition {
NSLog(@"转场取消");
}

- (void)finishInteractiveTransition {
NSLog(@"转场完成");
}


- (CALayer *)transitionMaskLayer {
if (_transitionMaskLayer == nil) {
_transitionMaskLayer = [CALayer layer];
}
return _transitionMaskLayer;
}

#pragma mark -
#pragma mark - private methods 私有方法
- (void)prepareGestureRecognizerInView:(UIView*)view {
UIPanGestureRecognizer *gesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleGesture:)];
[view addGestureRecognizer:gesture];
}

#pragma mark -
#pragma mark - event response 所有触发的事件响应 按钮、通知、分段控件等
- (void)handleGesture:(UIPanGestureRecognizer *)gestureRecognizer {
UIView *vcView = gestureRecognizer.view;
CGPoint translation = [gestureRecognizer translationInView:vcView.superview];
if(!self.isInteracting &&
(translation.x < 0 ||
translation.y < 0 ||
translation.x < translation.y)) {
return;
}
switch (gestureRecognizer.state) {
case UIGestureRecognizerStateBegan:{
//修复当从右侧向左滑动的时候的bug 避免开始的时候从又向左滑动 当未开始的时候
CGPoint vel = [gestureRecognizer velocityInView:gestureRecognizer.view];
if (!self.isInteracting && vel.x < 0) {
self.isInteracting = NO;
return;
}
self.transitionMaskLayer.frame = vcView.frame;
self.transitionMaskLayer.opaque = NO;
self.transitionMaskLayer.opacity = 1;
self.transitionMaskLayer.backgroundColor = [UIColor whiteColor].CGColor; //必须有颜色不能透明
[self.transitionMaskLayer setNeedsDisplay];
[self.transitionMaskLayer displayIfNeeded];
self.transitionMaskLayer.anchorPoint = CGPointMake(0.5, 0.5);
self.transitionMaskLayer.position = CGPointMake(vcView.frame.size.width/2.0f, vcView.frame.size.height/2.0f);
vcView.layer.mask = self.transitionMaskLayer;
vcView.layer.masksToBounds = YES;

self.isInteracting = YES;
}
break;
case UIGestureRecognizerStateChanged: {
CGFloat progress = translation.x / [UIScreen mainScreen].bounds.size.width;
progress = fminf(fmaxf(progress, 0.0), 1.0);

CGFloat ratio = 1.0f - progress*0.5f;
[_presentingVC.view setCenter:CGPointMake(_viewControllerCenter.x + translation.x * ratio, _viewControllerCenter.y + translation.y * ratio)];
_presentingVC.view.transform = CGAffineTransformMakeScale(ratio, ratio);
[self updateInteractiveTransition:progress];
break;
}
case UIGestureRecognizerStateCancelled:
case UIGestureRecognizerStateEnded:{
CGFloat progress = translation.x / [UIScreen mainScreen].bounds.size.width;
progress = fminf(fmaxf(progress, 0.0), 1.0);
if (progress < 0.2){
[UIView animateWithDuration:progress
delay:0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
CGFloat w = [UIScreen mainScreen].bounds.size.width;
CGFloat h = [UIScreen mainScreen].bounds.size.height;
[self.presentingVC.view setCenter:CGPointMake(w/2, h/2)];
self.presentingVC.view.transform = CGAffineTransformMakeScale(1.0f, 1.0f);
} completion:^(BOOL finished) {
self.isInteracting = NO;
[self cancelInteractiveTransition];
}];
}else {
_isInteracting = NO;
[self finishInteractiveTransition];
[_presentingVC dismissViewControllerAnimated:YES completion:nil];
}
//移除 遮罩
[self.transitionMaskLayer removeFromSuperlayer];
self.transitionMaskLayer = nil;
}
break;
default:
break;
}
}

#pragma mark -
#pragma mark - public methods 公有方法
-(void)wireToViewController:(UIViewController *)viewController {
self.presentingVC = viewController;
self.viewControllerCenter = viewController.view.center;
[self prepareGestureRecognizerInView:viewController.view];
}

@end


关键的核心代码

[self updateInteractiveTransition:progress];

最后 手势结束

CGFloat progress = translation.x / [UIScreen mainScreen].bounds.size.width;
progress = fminf(fmaxf(progress, 0.0), 1.0);
if (progress < 0.2){
[UIView animateWithDuration:progress
delay:0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
CGFloat w = [UIScreen mainScreen].bounds.size.width;
CGFloat h = [UIScreen mainScreen].bounds.size.height;
[self.presentingVC.view setCenter:CGPointMake(w/2, h/2)];
self.presentingVC.view.transform = CGAffineTransformMakeScale(1.0f, 1.0f);
} completion:^(BOOL finished) {
self.isInteracting = NO;
[self cancelInteractiveTransition];
}];
}else {
_isInteracting = NO;
[self finishInteractiveTransition];
[_presentingVC dismissViewControllerAnimated:YES completion:nil];
}
//移除 遮罩
[self.transitionMaskLayer removeFromSuperlayer];
self.transitionMaskLayer = nil;


demo及常见问题:https://github.com/sunyazhou13/AwemeDemoTransition



收起阅读 »

UITableView 建模

tableview 是开发中项目中常用的视图控件,并且是重复的使用,布局类似,只是数据源及Cell更改,所以会出现很多重复的内容,并且即使新建一个基础的列表也要重复这些固定逻辑的代码,这对于开发效率很不友好。本文的重点是抽取重复的逻辑代码,简化列表页面的搭建,...
继续阅读 »
tableview 是开发中项目中常用的视图控件,并且是重复的使用,布局类似,只是数据源及Cell更改,所以会出现很多重复的内容,并且即使新建一个基础的列表也要重复这些固定逻辑的代码,这对于开发效率很不友好。
本文的重点是抽取重复的逻辑代码简化列表页面的搭建,达到数据驱动列表

说明:
首先tableview有两个代理delegate 和 datasource(基于单一职责设计规则)
delegate :负责交互事件;
datasource :负责cell创建及数据填充,这也是本文探讨的重点。
(1)基本原则
苹果将tableView的数据通过一个二维数组构建(组,行),这是一个很重要的设计点,要沿着这套规则继续发展,设计模式的继承,才是避免坏代码产生的基础。
(2)组
“组”是这套逻辑的根基先有组再有行,并且列表动态修改的内容都是以为基础,的结构相对固定,因此本文将抽离成一个数据模型而不是接口


#import <Foundation/Foundation.h>
#import "RWCellViewModelProtocol.h"

@interface RWSectionModel : NSObject
/// item数组:元素必须是遵守RWCellViewModel协议
@property (nonatomic, strong) NSMutableArray <id<RWCellViewModel>>*itemsArray;

/// section头部高度
@property (nonatomic, assign) CGFloat sectionHeaderHeight;
/// section尾部高度
@property (nonatomic, assign) CGFloat sectionFooterHeight;
/// sectionHeaderView: 必须是UITableViewHeaderFooterView或其子类,并且遵循RWHeaderFooterDataSource协议
@property (nonatomic, strong) Class headerReuseClass;
/// sectionFooterView: 必须是UITableViewHeaderFooterView或其子类,并且遵循RWHeaderFooterDataSource协议
@property (nonatomic, strong) Class footerReuseClass;

/// headerData
@property (nonatomic, strong) id headerData;
/// footerData
@property (nonatomic, strong) id footerData;
@end

(2)行
最核心的有三大CellCell高度Cell数据
这次的设计参考MVVM设计模式,对于行的要素提取成一个ViewModel,并且ViewModel要做成接口的方式,因为行除了这三个基本的元素外,可能要需要Cell填充的数据,比如titleString,subTitleString,headerImage等等,这样便于扩展。


#ifndef RWCellViewModel_h
#define RWCellViewModel_h

@import UIKit;

@protocol RWCellViewModel <NSObject>
/// Cell 的类型
@property (nonatomic, strong) Class cellClass;
/// Cell的高度: 0 则是UITableViewAutomaticDimension
@property (nonatomic, assign) CGFloat cellHeight;
@end

#endif /* RWCellViewModel_h */

(3)tableView
此处不用使用tableViewController的方式,而使用view的方式,这样嵌入更方便。并且对外提供基本的接口,用于列表数据的获取,及点击事件处理。

备注:
关于数据,这里提供了多组和单组的两个接口,为了减少使用的过程中外部新建RWSectionModel这一步,但是其内部还是基于RWSectionModel这一个模型。


#import <UIKit/UIKit.h>
#import "RWCellViewModelProtocol.h"
#import "RWSectionModel.h"

@protocol RWTableViewDelegate;

@interface RWTableView : UITableView
/// rwdelegate
@property (nonatomic, weak) id<RWTableViewDelegate> rwdelegate;

/// 构建方法
/// @param delegate 是指rwdelegate
- (instancetype)initWithDelegate:(id<RWTableViewDelegate>)delegate;

@end


@protocol RWTableViewDelegate <NSObject>
@optional
/// 多组构建数据
- (NSArray <RWSectionModel*>*)tableViewWithMutilSectionDataArray;

/// 单组构建数据
- (NSArray <id<RWCellViewModel>>*)tableViewWithSigleSectionDataArray;


/// cell点击事件
/// @param data cell数据模型
/// @param indexPath indexPath
- (void)tableViewDidSelectedCellWithDataModel:(id)data indexPath:(NSIndexPath *)indexPath;

RWTableview.m

#pragma mark - dataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
/// 数据源始终保持“二维数组的状态”,即SectionModel中包裹items的方式
if ([self.rwdelegate respondsToSelector:@selector(tableViewWithMutilSectionDataArray)]) {
self.dataArray = [self.rwdelegate tableViewWithMutilSectionDataArray];
return self.dataArray.count;
}
else if ([self.rwdelegate respondsToSelector:@selector(tableViewWithSigleSectionDataArray)]) {
RWSectionModel *sectionModel = [[RWSectionModel alloc]init];
sectionModel.itemsArray = [self.rwdelegate tableViewWithSigleSectionDataArray].mutableCopy;
self.dataArray = @[sectionModel];
return 1;
}
return 0;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
RWSectionModel *sectionModel = [self.dataArray objectAtIndex:section];
return sectionModel.itemsArray.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
/// 此处只做Cell的复用或创建
RWSectionModel *sectionModel = [self.dataArray objectAtIndex:indexPath.section];
id<RWCellViewModel>cellViewModel = [sectionModel.itemsArray objectAtIndex:indexPath.row];
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass(cellViewModel.cellClass)];
if (cell == nil) {
cell = [[cellViewModel.cellClass alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:NSStringFromClass(cellViewModel.cellClass)];
}
cell.selectionStyle = UITableViewCellSelectionStyleNone;
return cell;
}

(4)Cell上子控件的交互事件处理
腾讯QQ部门的大神峰之巅提供了一个很好的解决办法,基于苹果现有的响应链(真的很牛逼),将点击事件传递给下个响应者,而不需要为事件的传递搭建更多的依赖关系。这是一篇鸡汤文章,有很多营养,比如tableview模块化,这也是我接下来要学习的。

#import <UIKit/UIKit.h>
#import "RWEvent.h"

@interface UIResponder (RWEvent)

- (void)respondEvent:(NSObject<RWEvent> *)event;

@end
#import "UIResponder+RWEvent.h"

@implementation UIResponder (RWEvent)

- (void)respondEvent:(NSObject<RWEvent> *)event {
[self.nextResponder respondEvent:event];
}

@end


2020年11月18日 更新

鉴于此tableView封装在实际项目遇到的问题进行改善,主要内容如下:
(1)使用分类的方式替换协议
优点:分类能更便捷的扩展原有类,并且使用更方便,不需要再导入协议文件及遵守协议
【RWCellDataSource协议】替换成:【UITableViewCell (RWData)】
【RWHeaderFooterDataSource协议】 替换成:【UITableViewHeaderFooterView (RWData)】

(2)cell高度缓存的勘误
willDisplayCell:中要想获取准确的Cell高度,那么必须在heightForRowAtIndexPath:方法中给Cell赋值,因为系统计算Cell的高度是在这个方法中进行的

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
RWSectionModel *sectionModel = [self.dataArray objectAtIndex:indexPath.section];
id<RWCellViewModel>cellViewModel = [sectionModel.itemsArray objectAtIndex:indexPath.row];
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass(cellViewModel.cellClass)];
/// Cell创建
if (cell == nil) {
cell = [[cellViewModel.cellClass alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:NSStringFromClass(cellViewModel.cellClass)];
}
/// Cell赋值
[cell rw_setData:cellViewModel];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
return cell;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
RWSectionModel *sectionModel = [self.dataArray objectAtIndex:indexPath.section];
id<RWCellViewModel>cellViewModel = [sectionModel.itemsArray objectAtIndex:indexPath.row];
return cellViewModel.cellHeight ? : UITableViewAutomaticDimension;
}

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
RWSectionModel *sectionModel = [self.dataArray objectAtIndex:indexPath.section];
id<RWCellViewModel>cellViewModel = [sectionModel.itemsArray objectAtIndex:indexPath.row];
/// 高度缓存
/// 此处高度做一个缓存是为了高度自适应的Cell,避免重复计算的工作量,对于性能优化有些帮助
/// 如果想要在willDisplayCell获取到准确的Cell高度,那么必须在cellForRowAtIndexPath:方法给Cell赋值
/// 同时可以避免由于高度自适应导致Cell的定位不准确,比如置顶或者滑动到某一个Cell的位置
/// 如果自动布局要更新高度,可以将cellViewModel设置为0
cellViewModel.cellHeight = cell.frame.size.height;
}













收起阅读 »

Flutter中的异步

同步与异步 程序的运行是出于满足人们对某种逻辑需求的处理,在计算机上表现为可执行指令,正常情况下我们期望的指令是按逻辑的顺序依次执行的,而实际情况由于某些指令是耗时操作,不能立即返回结果而造成了阻塞,导致程序无法继续执行。这种情况多见于一些io操作。这时,对...
继续阅读 »

同步与异步


程序的运行是出于满足人们对某种逻辑需求的处理,在计算机上表现为可执行指令,正常情况下我们期望的指令是按逻辑的顺序依次执行的,而实际情况由于某些指令是耗时操作,不能立即返回结果而造成了阻塞,导致程序无法继续执行。这种情况多见于一些io操作。这时,对于用户层面来说,我们可以选择stop the world,等待操作完成返回结果后再继续操作,也可以选择继续去执行其他操作,等事件返回结果后再通知回来。这就是从用户角度来看的同步与异步。


从操作系统的角度,同步异步,与任务调度,进程间切换,中断,系统调用之间有着更为复杂的关系。


同步I/O 与 异步I/O的区别


img

为什么使用异步


用户可以阻塞式的等待,因为人的操作和计算机相比是非常慢的,计算机如果阻塞那就是很大的性能浪费了,异步操作让您的程序在等待另一个操作的同时完成工作。三种异步操作的场景:



  • I/O操作:例如:发起一个网络请求,读写数据库、读写文件、打印文档等,一个同步的程序去执行这些操作,将导致程序的停止,直到操作完成。更有效的程序会改为在操作挂起时去执行其他操作,假设您有一个程序读取一些用户输入,进行一些计算,然后通过电子邮件发送结果。发送电子邮件时,您必须向网络发送一些数据,然后等待接收服务器响应。等待服务器响应所投入的时间是浪费的时间,如果程序继续计算,这将得到更好的利用

  • 并行执行多个操作:当您需要并行执行不同的操作时,例如进行数据库调用、Web 服务调用以及任何计算,那么我们可以使用异步

  • 长时间运行的基于事件驱动的请求:这就是您有一个请求进来的想法,并且该请求进入休眠状态一段时间等待其他一些事件的发生。当该事件发生时,您希望请求继续,然后向客户端发送响应。所以在这种情况下,当请求进来时,线程被分配给该请求,当请求进入睡眠状态时,线程被发送回线程池,当任务完成时,它生成事件并从线程池中选择一个线程发送响应


计算机中异步的实现方式就是任务调度,也就是进程的切换


任务调度采用的是时间片轮转的抢占式调度方式,进程是任务调度的最小单位。


计算机系统分为用户空间内核空间,用户进程在用户空间,操作系统运行在内核空间,内核空间的数据访问修改拥有高于普通进程的权限,用户进程之间相互独立,内存不共享,保证操作系统的运行安全。如何最大化的利用CPU,确定某一时刻哪个进程拥有CPU资源就是任务调度的过程。内核负责调度管理用户进程,以下为进程调度过程


img

在任意时刻, 一个 CPU 核心上(processor)只可能运行一个进程



每一个进程可以包含多个线程,线程是执行操作的最小单元,因此进程的切换落实到具体细节就是正在执行线程的切换


Future


Future<T> 表示一个异步的操作结果,用来表示一个延迟的计算,返回一个结果或者error,使用代码实例:


Future<int> future = getFuture();
future.then((value) => handleValue(value))
.catchError((error) => handleError(error))
.whenComplete(func);

future可以是三种状态:未完成的返回结果值返回异常


当一个返回future对象被调用时,会发生两件事:



  • 将函数操作入队列等待执行结果并返回一个未完成的Future对象

  • 函数操作完成时,Future对象变为完成并携带一个值或一个错误


首先,Flutter事件处理模型为先执行main函数,完成后检查执行微任务队列Microtask Queue中事件,最后执行事件队列Event Queue中的事件,示例:


void main(){
Future(() => print(10));
Future.microtask(() => print(9));
print("main");
}
/// 打印结果为:
/// main
/// 9
/// 10

基于以上事件模型的基础上,看下Future提供的几种构造函数,其中最基本的为直接传入一个Function


factory Future(FutureOr<T> computation()) {
_Future<T> result = new _Future<T>();
Timer.run(() {
try {
result._complete(computation());
} catch (e, s) {
_completeWithErrorCallback(result, e, s);
}
});
return result;
}

Function有多种写法:


//简单操作,单步
Future(() => print(5));
//稍复杂,匿名函数
Future((){
print(6);
});
//更多操作,方法名
Future(printSeven);

printSeven(){
print(7);
}


Future.microtask


此工程方法创建的事件将发送到微任务队列Microtask Queue,具有相比事件队列Event Queue优先执行的特点


factory Future.microtask(FutureOr<T> computation()) {
_Future<T> result = new _Future<T>();
//
scheduleMicrotask(() {
try {
result._complete(computation());
} catch (e, s) {
_completeWithErrorCallback(result, e, s);
}
});
return result;
}

Future.sync


返回一个立即执行传入参数的Future,可理解为同步调用


factory Future.sync(FutureOr<T> computation()) {
try {
var result = computation();
if (result is Future<T>) {
return result;
} else {
// TODO(40014): Remove cast when type promotion works.
return new _Future<T>.value(result as dynamic);
}
} catch (error, stackTrace) {
/// ...
}
}

	Future.microtask(() => print(9));
Future(() => print(10));
Future.sync(() => print(11));

/// 打印结果: 11、9、10

Future.value


创建一个将来包含value的future


factory Future.value([FutureOr<T>? value]) {
return new _Future<T>.immediate(value == null ? value as T : value);
}

参数FutureOr含义为T value 和 Future value 的合集,因为对于一个Future参数来说,他的结果可能为value或者是Future,所以对于以下两种写法均合法:


	Future.value(12).then((value) => print(value));
Future.value(Future<int>((){
return 13;
}));


这里需要注意即使value接收的是12,仍然会将事件发送到Event队列等待执行,但是相对其他Future事件执行顺序会提前



Future.error


创建一个执行结果为error的future


factory Future.error(Object error, [StackTrace? stackTrace]) {
/// ...
return new _Future<T>.immediateError(error, stackTrace);
}

_Future.immediateError(var error, StackTrace stackTrace)
: _zone = Zone._current {
_asyncCompleteError(error, stackTrace);
}

 Future.error(new Exception("err msg"))
.then((value) => print("err value: $value"))
.catchError((e) => print(e));

/// 执行结果为:Exception: err msg

Future.delayed


创建一个延迟执行回调的future,内部实现为Timer加延时执行一个Future


factory Future.delayed(Duration duration, [FutureOr<T> computation()?]) {
/// ...
new Timer(duration, () {
if (computation == null) {
result._complete(null as T);
} else {
try {
result._complete(computation());
} catch (e, s) {
_completeWithErrorCallback(result, e, s);
}
}
});
return result;
}

Future.wait


等待多个Future并收集返回结果


static Future<List<T>> wait<T>(Iterable<Future<T>> futures,
{bool eagerError = false, void cleanUp(T successValue)?}) {
/// ...
}

FutureBuilder结合使用:


child: FutureBuilder(
future: Future.wait([
firstFuture(),
secondFuture()
]),
builder: (context,snapshot){
if(!snapshot.hasData){
return CircularProgressIndicator();
}
final first = snapshot.data[0];
final second = snapshot.data[1];
return Text("data $first $second");
},
),

Future.any


返回futures集合中第一个返回结果的值


static Future<T> any<T>(Iterable<Future<T>> futures) {
var completer = new Completer<T>.sync();
void onValue(T value) {
if (!completer.isCompleted) completer.complete(value);
}
void onError(Object error, StackTrace stack) {
if (!completer.isCompleted) completer.completeError(error, stack);
}
for (var future in futures) {
future.then(onValue, onError: onError);
}
return completer.future;
}

对上述例子来说,Future.any snapshot.data 将返回firstFuturesecondFuture中第一个返回结果的值


Future.forEach


为传入的每一个元素,顺序执行一个action


static Future forEach<T>(Iterable<T> elements, FutureOr action(T element)) {
var iterator = elements.iterator;
return doWhile(() {
if (!iterator.moveNext()) return false;
var result = action(iterator.current);
if (result is Future) return result.then(_kTrue);
return true;
});
}

这里边action是方法作为参数,头一次见这种形式语法还是在js中,当时就迷惑了很大一会儿,使用示例:


Future.forEach(["one","two","three"], (element) {
print(element);
});

Future.doWhile


执行一个操作直到返回false


Future.doWhile((){
for(var i=0;i<5;i++){
print("i => $i");
if(i >= 3){
return false;
}
}
return true;
});
/// 结果打印到 3

以上为Future中常用构造函数和方法


在Widget中使用Future


Flutter提供了配合Future显示的组件FutureBuilder,使用也很简单,伪代码如下:


child: FutureBuilder(
future: getFuture(),
builder: (context, snapshot){
if(!snapshot.hasData){
return CircularProgressIndicator();
} else if(snapshot.hasError){
return _ErrorWidget("Error: ${snapshot.error}");
} else {
return _ContentWidget("Result: ${snapshot.data}")
}
}
)

Async-await


使用


这两个关键字提供了异步方法的同步书写方式,Future提供了方便的链式调用使用方式,但是不太直观,而且大量的回调嵌套造成可阅读性差。因此,现在很多语言都引入了await-async语法,学习他们的使用方式是很有必要的。


两条基本原则:



  • 定义一个异步方法,必须在方法体前声明 async

  • await关键字必须在async方法中使用


首先,在要执行耗时操作的方法体前增加async:


void main() async { ··· }

然后,根据方法的返回类型添加Future修饰


Future<void> main() async { ··· }

现在就可以使用await关键字来等待这个future执行完毕


print(await createOrderMessage());

例如实现一个由一级分类获取二级分类,二级分类获取详情的需求,使用链式调用的代码如下:


var list = getCategoryList();
list.then((value) => value[0].getCategorySubList(value[0].id))
.then((subCategoryList){
var courseList = subCategoryList[0].getCourseListByCategoryId(subCategoryList[0].id);
print(courseList);
}).catchError((e) => (){
print(e);
});

现在来看下使用async/await,事情变得简单了多少


Future<void> main() async {
await getCourses().catchError((e){
print(e);
});
}
Future<void> getCourses() async {
var list = await getCategoryList();
var subCategoryList = await list[0].getCategorySubList(list[0].id);
var courseList = subCategoryList[0].getCourseListByCategoryId(subCategoryList[0].id);
print(courseList);
}

可以看到这样更加直观


缺陷


async/await 非常方便,但是还是有一些缺点需要注意


因为它的代码看起来是同步的,所以是会阻塞后面的代码执行,直到await返回结果,就像执行同步操作一样。它确实可以允许其他任务在此期间继续运行,但后边自己的代码被阻塞。


这意味着代码可能会由于有大量await代码相继执行而阻塞,本来用Future编写表示并行的操作,现在使用await变成了串行,例如,首页有一个同时获取轮播接口,tab列表接口,msg列表接口的需求


Future<String> getBannerList() async {
return await Future.delayed(Duration(seconds: 3),(){
return "banner list";
});
}

Future<String> getHomeTabList() async {
return await Future.delayed(Duration(seconds: 3),(){
return "tab list";
});
}

Future<String> getHomeMsgList() async {
return await Future.delayed(Duration(seconds: 3),(){
return "msg list";
});
}

使用await编写很可能会写成这样,打印执行操作的时间


Future<void> main2() async {
var startTime = DateTime.now().second;
await getBannerList();
await getHomeTabList();
await getHomeMsgList();
var endTime = DateTime.now().second;
print(endTime - startTime); // 9
}

在这里,我们直接等待所有三个模拟接口的调用,使每个调用3s。后续的每一个都被迫等到上一个完成, 最后会看到总运行时间为9s,而实际我们想三个请求同时执行,代码可以改成如下这种:


Future<void> main() async {
var startTime = DateTime.now().second;
var bannerList = getBannerList();
var homeTabList = getHomeTabList();
var homeMsgList = getHomeMsgList();

await bannerList;
await homeTabList;
await homeMsgList;
var endTime = DateTime.now().second;
print(endTime - startTime); // 3
}

将三个Future存储在变量中,这样可以同时启动,最后打印时间仅为3s,所以在编写代码时,我们必须牢记这点,避免性能损耗。


原理


线程模型


当一个Flutter应用或者Flutter Engine启动时,它会启动(或者从池中选择)另外三个线程,这些线程有些时候会有重合的工作点,但是通常,它们被称为UI线程GPU线程IO线程。需要注意一点这个UI线程并不是程序运行的主线程,或者说和其他平台上的主线程理解不同,通常的,Flutter将平台的主线程叫做"Platform thread"


img


UI线程是所有的Dard代码运行的地方,例如framework和你的应用,除非你启动自己的isolates,否则Dart将永远不会运行在其他线程。平台线程是所有依赖插件的代码运行的地方。该线程也是native frameworks为其他任务提供服务的地方,一般来说,一个Flutter应用启动的时候会创建一个Engine实例,Engine创建的时候会创建一个Platform thread为其提供服务。跟Flutter Engine的所有交互(接口调用)必须发生在Platform Thread,试图在其它线程中调用Flutter Engine会导致无法预期的异常。这跟Android/iOS UI相关的操作都必须在主线程进行相类似。


Isolates是Dart中概念,本意是隔离,它的实现功能和thread类似,但是他们之间的实现又有着本质的区别,Isolote是独立的工作者,它们之间不共享内存,而是通过channel传递消息。Dart是单线程执行代码,Isolate提供了Dart应用可以更好的利用多核硬件的解决方案。


事件循环


单线程模型中主要就是在维护着一个事件循环(Event Loop) 与 两个队列(event queue和microtask queue)当Flutter项目程序触发如点击事件IO事件网络事件时,它们就会被加入到eventLoop中,eventLoop一直在循环之中,当主线程发现事件队列不为空时发现,就会取出事件,并且执行。


microtask queue中事件优先于event queue执行,当有任务发送到microtask队列时,会在当前event执行完成后,阻塞当前event queue转而去执行microtask queue中的事件,这样为Dart提供了任务插队的解决方案。


event queue的阻塞意味着app无法进行UI绘制,响应鼠标和I/O等事件,所以要谨慎使用,如下为流程图:


event queue和microtask queue


这两个任务队列中的任务切换在某些方面就相当于是协程调度机制


协程


协程是一种协作式的任务调度机制,区别于操作系统的抢占式任务调度机制,它是用户态下面的,避免线程切换的内核态、用户态转换的性能开销。它让调用者自己来决定什么时候让出cpu,比操作系统的抢占式调度所需要的时间代价要小很多,后者为了恢复现场会保存相当多的状态(不仅包括进程上下文的虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态),并且会频繁的切换,以现在流行的大多数Linux机器来说,每一次的上下文切换要消耗大约1.2-1.5μs的时间,这是仅考虑直接成本,固定在单个核心以避免迁移的成本,未固定情况下,切换时间可达2.2μs


img

对cpu来说这算一个很长的时间吗,一个很好的比较是memcpy,在相同的机器上,完成一个64KiB数据的拷贝需要3μs的时间,上下文的切换比这个操作稍微快一些


Plot of thread/process launch and context switch

协程和线程非常相似,是从异步执行任务的角度来看,而并不是从设计的实体角度像进程->线程->协程这样类似于细胞->原子核->质子中子这样的关系。可以理解为线程上执行的一段函数,用yield完成异步请求、注册回调/通知器、保存状态,挂起控制流、收到回调/通知、恢复状态、恢复控制流的所有过程


多线程执行任务模型如图:



线程的阻塞要靠系统间进程的切换,完成逻辑流的执行,频繁的切换耗费大量资源,而且逻辑流的执行数量严重依赖于程序申请到的线程的数量。


协程是协同多任务的,这意味着协程提供并发性但不提供并行性,执行流模型图如下:



协程可以用逻辑流的顺序去写控制流,协程的等待会主动释放cpu,避免了线程切换之间的等待时间,有更好的性能,逻辑流的代码编写和理解上也简单的很多


但是线程并不是一无是处,抢占式线程调度器事实上提供了准实时的体验。例如Timer,虽然不能确保在时间到达的时候一定能够分到时间片运行,但不会像协程一样万一没有人让出时间片就永远得不到运行……


总结



  • 同步与异步

  • Future提供了Flutter中异步代码链式编写方式

  • async-wait提供了异步代码的同步书写方式

  • Future的常用方法和FutureBuilder编写UI

  • Flutter中线程模型,四个线程

  • 单线程语言的事件驱动模型

  • 进程间切换和协程对比



收起阅读 »

Protobuf 和 JSON对比分析

Protocol Buffers (a.k.a., protobuf) are Google's language-neutral, platform-neutral, extensible mechanism for serializing structur...
继续阅读 »

Protocol Buffers (a.k.a., protobuf) are Google's language-neutral, platform-neutral, extensible mechanism for serializing structured data.


Protobuf是Google公司开发的一种语言中立 平台中立 可扩展 的 对结构化数据 序列化的机制。


本文主要对Protobuf和JSON序列化&反序列化的性能做横向对比分析。 JSON序列化使用Google官方的Gson框架。


ProtobufGsonLanguagePlatform
3.17.32.8.7KotlinmacOS IntelliJ IDEA


测试序列化内容,高效作业25分钟的训练数据(mock)


数据结构


syntax = "proto3";
package me.sunnyxibei.data;
option java_package = "me.sunnyxibei.data";
option java_outer_classname = "TaskProto";

message Eeg{
repeated double alphaData = 1;
repeated double betaData = 2;
repeated double attentionData = 3;
repeated int64 timestampData = 4;
int64 startTimestamp = 5;
int64 endTimestamp = 6;
}
message TaskRecord{
string localId = 1;
int64 localCreated = 2;
int64 localUpdated = 3;
int32 score = 4;
int64 originDuration = 5;
string subject = 6;
string content = 7;
Eeg eeg = 8;
}

对比结果 repeat = 1


Gson序列化大小 = 30518 bytes
Gson序列化时间 = 113 ms
protobuf序列化大小 = 13590 bytes
protobuf序列化时间 = 39 ms
*************************
Gson反序列化时间 = 15 ms
protobuf反序列化时间 = 3 ms

repeat = 10


Gson序列化时间 = 137 ms
protobuf序列化时间 = 41 ms
*************************
Gson反序列化时间 = 50 ms
protobuf反序列化时间 = 5 ms

repeat = 100


Gson序列化时间 = 347 ms
protobuf序列化时间 = 47 ms
*************************
Gson反序列化时间 = 212 ms
protobuf反序列化时间 = 22 ms

repeat = 1000


Gson序列化时间 = 984 ms
protobuf序列化时间 = 97 ms
*************************
Gson反序列化时间 = 817 ms
protobuf反序列化时间 = 105 ms

repeat = 10000


Gson序列化时间 = 7034 ms
protobuf序列化时间 = 225 ms
*************************
Gson反序列化时间 = 5544 ms
protobuf反序列化时间 = 300 ms

repeat = 100000


Gson序列化时间 = 65560 ms
protobuf序列化时间 = 1469 ms
*************************
Gson反序列化时间 = 49984 ms
protobuf反序列化时间 = 2409 ms

结论:



  1. 空间对比,Protobuf序列化后的数据大小,为JSON序列化后的44.5%

  2. 时间对比


次数序列化(Protobuf/JSON)反序列化(Protobuf/JSON)
134.5%20%
1029.9%10%
10013.5%9.43%
10009.9%12.9%
100003.2%5.41%
1000002.24%4.82%


收起阅读 »

CoordinatorLayout 嵌套Recycleview 卡顿问题

1.问题场景 伪代码: <CoordinatorLayout> <AppBarLayout> <RecycleView> </RecycleView> </AppBa...
继续阅读 »

1.问题场景


伪代码:
<CoordinatorLayout>
<AppBarLayout>
<RecycleView>
</RecycleView>
</AppBarLayout>
</ConstraintLayout>

一般这种做法是,底部view的相应滑动,滑动联动,但是同时会出现RecycleView ViewHoder复用失败,造成cpu 的消耗,item到达一定数量后会造成oom页面出现卡顿


2. 问题原理


RecycleView ViewHoder 复用问题第一时间我们应想到是; ViewGrop/onMeasureChild
测量问题,重写 onMeasureChild ,避免中间MeasureSpec.UNSPECIFIED模式 的赋值造成RecycleView的item复用,但是是失败的!


 @Override
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
child.measure(parentWidthMeasureSpec, parentHeightMeasureSpec);
}


原因是: parentHeightMeasureSpec 已经被设置 MeasureSpec.UNSPECIFIED 测量模式 看下源码CoordinatorLayout onMeasure 局部关键代码:


prepareChildren();

final Behavior b = lp.getBehavior();
if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0)) {
onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0);
}

通过prepareChildren()结合LayoutParams


        R.styleable.CoordinatorLayout_Layout_layout_behavior);
if (mBehaviorResolved) {
mBehavior = parseBehavior(context, attrs, a.getString(
R.styleable.CoordinatorLayout_Layout_layout_behavior));
}

我们可以得到 Behavior b 就是我们再布局内设置的 AppBarLayout. layout_behavior, 可以看到 Behavior/onMeasureChild 做了一层测量, ,我们继续看 Behavior/onMeasureChild 源码:


@Override
public boolean onMeasureChild(
@NonNull CoordinatorLayout parent,
@NonNull T child,
int parentWidthMeasureSpec,
int widthUsed,
int parentHeightMeasureSpec,
int heightUsed) {
final CoordinatorLayout.LayoutParams lp =
(CoordinatorLayout.LayoutParams) child.getLayoutParams();
if (lp.height == CoordinatorLayout.LayoutParams.WRAP_CONTENT) {
// If the view is set to wrap on it's height, CoordinatorLayout by default will
// cap the view at the CoL's height. Since the AppBarLayout can scroll, this isn't
// what we actually want, so we measure it ourselves with an unspecified spec to
// allow the child to be larger than it's parent
parent.onMeasureChild(
child,
parentWidthMeasureSpec,
widthUsed,
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
heightUsed);
return true;
}

// Let the parent handle it as normal
return super.onMeasureChild(
parent, child, parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed);
}

问题找到了问题关键 if (lp.height == CoordinatorLayout.LayoutParams.WRAP_CONTENT) ,造成了MeasureSpec.UNSPECIFIED 的使用, 而这个模式又会造成Recycleview.LayoutManager加载所有的item,导致复用失败; 看到这 AppBarLayout给固定值或者match_parent 不就解决问题了吗, 是能解决问题,但是这样 我们的layout ui就不符合我们绘制ui的布局了,也会造成页面空白显示问题,所以这样使用recycleview 嵌套是非法使用,矛盾使用!


解决问题



  • 同一使用RecycleView 使用,作为RecycleView item 的一部分,但是也会造成滑动冲突问题,然后通过 NestedScrollingParent3 外部拦截法,来解决内外层的滑动冲突,问题顺利解决


override fun onInterceptTouchEvent(e: MotionEvent?): Boolean {
if (e!!.action == MotionEvent.ACTION_DOWN) {
val childRecyclerView = findCurrentChildRecyclerView()

// 1. 是否禁止拦截
doNotInterceptTouchEvent = doNotInterceptTouch(e.rawY, childRecyclerView)

// 2. 停止Fling
this.stopFling()
childRecyclerView?.stopFling()
}

return if (doNotInterceptTouchEvent) {
false
} else {
super.onInterceptTouchEvent(e)
}
}


  • 根据业务场景,也可使用baserecyclerviewadapterhelper,一个优秀的Adapter 框架, 


addHeaderView来添加itemView,通过 notifyItemInserted(position) 添加ReceiveView 的item


@JvmOverloads
fun addHeaderView(view: View, index: Int = -1, orientation: Int = LinearLayout.VERTICAL): Int {
if (!this::mHeaderLayout.isInitialized) {
mHeaderLayout = LinearLayout(view.context)
mHeaderLayout.orientation = orientation
mHeaderLayout.layoutParams = if (orientation == LinearLayout.VERTICAL) {
RecyclerView.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
} else {
RecyclerView.LayoutParams(WRAP_CONTENT, MATCH_PARENT)
}
}

val childCount = mHeaderLayout.childCount
var mIndex = index
if (index < 0 || index > childCount) {
mIndex = childCount
}
mHeaderLayout.addView(view, mIndex)
if (mHeaderLayout.childCount == 1) {
val position = headerViewPosition
if (position != -1) {
notifyItemInserted(position)
}
}
return mIndex
}
收起阅读 »

优雅地封装 Activity Result API,完美地替代 startActivityForResult()

前言 Activity Result API。这是官方用于替代 startActivityForResult() 和 onActivityResult() 的。虽然出了有大半年了,但是个人到现在没看到比较好用的封装。最初大多数人会用拓展函数进行封装,而在 a...
继续阅读 »

前言


Activity Result API。这是官方用于替代 startActivityForResult()onActivityResult() 的。虽然出了有大半年了,但是个人到现在没看到比较好用的封装。最初大多数人会用拓展函数进行封装,而在 activity-ktx:1.2.0-beta02 版本之后,调用注册方法的时机必须在 onStart() 之前,原来的拓展函数就不适用了,在这之后就没看到有人进行封装了。


个人对 Activity Result API 的封装思考了很久,已经尽量做到在 Kotlin 和 Java 都足够地好用,可以完美替代 startActivityForResult() 了。下面带着大家一起来封装 Activity Result API。


基础用法


首先要先了解基础的用法,在 ComponentActivity 或 Fragment 中调用 Activity Result API 提供的 registerForActivityResult() 方法注册结果回调(在 onStart() 之前调用)。该方法接收 ActivityResultContract 和 ActivityResultCallback 参数,返回可以启动另一个 activity 的 ActivityResultLauncher 对象。


ActivityResultContract 协议类定义生成结果所需的输入类型以及结果的输出类型,Activity Result API 已经提供了很多默认的协议类,方便大家实现请求权限、拍照等常见操作。


val getContent = registerForActivityResult(GetContent()) { uri: Uri? ->
 // Handle the returned Uri
}

只是注册回调并不会启动另一个 activity ,还要调用 ActivityResultLauncher#launch() 方法才会启动。传入协议类定义的输入参数,当用户完成后续 activity 的操作并返回时,将执行 ActivityResultCallback 中的 onActivityResult()回调方法。


getContent.launch("image/*")

完整的使用代码:


val getContent = registerForActivityResult(GetContent()) { uri: Uri? ->
  // Handle the returned Uri
}

override fun onCreate(savedInstanceState: Bundle?) {
  // ...
  selectButton.setOnClickListener {
    getContent.launch("image/*")
  }
}

ActivityResultContracts 提供了许多默认的协议类:


协议类作用
RequestPermission()请求单个权限
RequestMultiplePermissions()请求多个权限
TakePicturePreview()拍照预览,返回 Bitmap
TakePicture()拍照,返回 Uri
TakeVideo()录像,返回 Uri
GetContent()获取单个内容文件
GetMultipleContents()获取多个内容文件
CreateDocument()创建文档
OpenDocument()打开单个文档
OpenMultipleDocuments()打开多个文档
OpenDocumentTree()打开文档目录
PickContact()选择联系人
StartActivityForResult()通用协议


我们还可以自定义协议类,继承 ActivityResultContract,定义输入和输出类。如果不需要任何输入,可使用 Void 或 Unit 作为输入类型。需要实现两个方法,用于创建与 startActivityForResult() 配合使用的 Intent 和解析输出的结果。


class PickRingtone : ActivityResultContract<Int, Uri?>() {
  override fun createIntent(context: Context, ringtoneType: Int) =
    Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply {
      putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, ringtoneType)
    }

  override fun parseResult(resultCode: Int, result: Intent?) : Uri? {
    if (resultCode != Activity.RESULT_OK) {
      return null
    }
    return result?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
  }
}

自定义协议类实现后,就能调用注册方法和 launch() 方法进行使用。


val pickRingtone = registerForActivityResult(PickRingtone()) { uri: Uri? ->
  // Handle the returned Uri
}

pickRingtone.launch(ringtoneType)

不想自定义协议类的话,可以使用通用的协议 ActivityResultContracts.StartActivityForResult(),实现类似于之前 startActivityForResult() 的功能。


val startForResult = registerForActivityResult(StartActivityForResult()) { result: ActivityResult ->
  if (result.resultCode == Activity.RESULT_OK) {
      val intent = result.intent
      // Handle the Intent
  }
}

startForResult.launch(Intent(this, InputTextActivity::class.java))

封装思路


为什么要封装?


看完上面的用法,不知道大家会不会和我初次了解的时候一样,感觉比原来复杂很多。


主要是引入的新概念比较多,原来只需要了解 startActivityForResult()onActivityResult() 的用法,现在要了解一大堆类是做什么的,学习成本高了不少。


用法也有些奇怪,比如官方示例用注册方法得到一个叫 getContent 对象,这更像是函数的命名,还要用这个对象去调用 launch() 方法,代码阅读起来总感觉怪怪的。


而且有个地方个人觉得不是很好,callback 居然在 registerForActivityResult() 方法里传。个人觉得 callback 在 launch() 方法里传更符合习惯,逻辑也更加连贯,代码阅读性更好。最好改成下面的用法,启动后就接着处理结果的逻辑。


getContent.launch("image/*") { uri: Uri? ->
 // Handle the returned Uri
}

所以还是有必要对 Activity Result API 进行封装的。


怎么封装?


首先是修改 callback 传参的位置,实现思路也比较简单,重载 launch() 方法加一个 callback 参数,用个变量缓存起来。在回调的时候拿缓存的 callback 对象去执行。


private var callback: ActivityResultCallback? = null

fun launch(input: I?, callback: ActivityResultCallback<O>) {
 this.callback = callback
 launcher.launch(input)
}

由于需要缓存 callback 对象,还要写一个类来持有该缓存变量。


有一个不好处理的问题是 registerForActivityResult() 需要的 onStart() 之前调用。可以通过 lifecycle 在 onCreate() 的时候自动注册,但是个人思考了好久并没有想到更优的实现方式。就是获取 lifecycleOwner 观察声明周期自动注册,也是需要在 onStart() 之前调用,那为什么不直接执行注册方法呢?所以个人改变了思路,不纠结于自动注册,而是简化注册的代码。


前面说了需要再写一个类缓存 callback 对象,使用一个类的时候有个方法基本会用到,就是构造函数。我们可以在创建对象的时候进行注册。


注册方法需要 callback 和协议类对象两个参数,callback 是从 launch() 方法得到,而协议类对象就需要传了。这样用起来个人觉得还不够友好,综合考虑后决定用继承的方式把协议类对象给“隐藏”了。


最终得到以下的基类。


public class BaseActivityResultLauncher<I, O> {

 private final ActivityResultLauncher launcher;
 private ActivityResultCallback callback;

 public BaseActivityResultLauncher(ActivityResultCaller caller, ActivityResultContract contract) {
   launcher = caller.registerForActivityResult(contract, (result) -> {
     if (callback != null) {
       callback.onActivityResult(result);
       callback = null;
    }
  });
}

 public void launch(@SuppressLint("UnknownNullness") I input, @NonNull ActivityResultCallback callback) {
   this.callback = callback;
   launcher.launch(input);
}
}

改用了 Java 代码来实现,返回的结果可以判空也可以不判空,比如返回数组的时候一定不为空,只是数组大小为 0 。用 Kotlin 实现的话要写两个不同名的方法来应对这个情况,使用起来并不是很方便。


这是多增加一个封装的步骤来简化后续的使用,原本只是继承 ActivityResultContract 实现协议类,现在还需要再写一个启动器类继承 BaseActivityResultLauncher


比如用前面获取图片的示例,我们再封装一个 GetContentLauncher 类。


class GetContentLauncher(caller: ActivityResultCaller) :
BaseActivityResultLauncher(caller, GetContent())

只需这么简单的继承封装,后续使用就更加简洁易用了。


val getContentLauncher = GetContentLauncher(this)

override fun onCreate(savedInstanceState: Bundle?) {
  // ...
  selectButton.setOnClickListener {
    getContentLauncher.launch("image/*") { uri: Uri? ->
  // Handle the returned Uri
}
  }
}

再封装一个 Launcher 类的好处是,能更方便地重载 launch() 方法,比如在类里增加一个方法在获取图片之前会先授权读取权限。如果改用 Kotlin 拓展函数来实现,在 Java 会更加难用。Launcher 类能对 Java 用法进行兼顾。


最后总结一下,对比原本 Activity Result API 的用法,改善了什么问题:



  • 简化冗长的注册代码,改成简单地创建一个对象;

  • 改善对象的命名,比如官方示例命名为 getContent 对象就很奇怪,这通常是函数的命名。优化后很自然地用类名来命名为 getContentLauncher,使用一个启动器对象调用 launch() 方法会更加合理;

  • 改变回调的位置,使其更加符合使用习惯,逻辑更加连贯,代码阅读性更好;

  • 输入参数和输出参数不会限制为一个对象,可以重载方法简化用法;

  • 能更方便地整合多个启动器的功能,比如获取读取权限后再跳转相册选择图片;


最终用法


由于 Activity Result API 已有很多的协议类,如果每一个协议都去封装一个启动器类会有点麻烦,所以个人已经写好一个库 ActivityResultLauncher 方便大家使用。还新增和完善了一些功能,有以下特点:



  • 完美替代 startActivityForResult()

  • 支持 Kotlin 和 Java 用法

  • 支持请求权限

  • 支持拍照

  • 支持录像

  • 支持选择图片或视频(已适配 Android 10)

  • 支持裁剪图片(已适配 Android11)

  • 支持打开蓝牙

  • 支持打开定位

  • 支持使用存储访问框架 SAF

  • 支持选择联系人


个人写了个 Demo 给大家来演示有什么功能,完整的代码在 Github 里。


demo-qr-code.png


screenshot


下面来介绍 Kotlin 的用法,Java 的用法可以查看 Wiki 文档


在根目录的 build.gradle 添加:


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

添加依赖:


dependencies {
   implementation 'com.github.DylanCaiCoding:ActivityResultLauncher:1.0.0'
}

用法也只有简单的两步:


第一步,在 ComponentActivityFragment 创建对应的对象,需要注意创建对象的时机要在 onStart() 之前。例如创建通用的启动器:


private val startActivityLauncher = StartActivityLauncher(this)

提供以下默认的启动器类:

启动器作用
StartActivityLauncher完美替代 startActivityForResult()
TakePicturePreviewLauncher调用系统相机拍照预览,只返回 Bitmap
TakePictureLauncher调用系统相机拍照
TakeVideoLauncher调用系统相机录像
PickContentLauncher, GetContentLauncher选择单个图片或视频,已适配 Android 10
GetMultipleContentsLauncher选择多个图片或视频,已适配 Android 10
CropPictureLauncher裁剪图片,已适配 Android 11
RequestPermissionLauncher请求单个权限
RequestMultiplePermissionsLauncher请求多个权限
AppDetailsSettingsLauncher打开系统设置的 App 详情页
EnableBluetoothLauncher打开蓝牙
EnableLocationLauncher打开定位
CreateDocumentLauncher创建文档
OpenDocumentLauncher打开单个文档
OpenMultipleDocumentsLauncher打开多个文档
OpenDocumentTreeLauncher访问目录内容
PickContactLauncher选择联系人
StartIntentSenderLauncher替代 startIntentSender()


第二步,调用启动器对象的 launch() 方法。


比如跳转一个输入文字的页面,点击保存按钮回调结果。我们替换掉原来 startActivityForResult() 的写法。


val intent = Intent(this, InputTextActivity::class.java)
intent.putExtra(KEY_NAME, "nickname")
startActivityLauncher.launch(intent) { activityResult ->
if (activityResult.resultCode == RESULT_OK) {
data?.getStringExtra(KEY_VALUE)?.let { toast(it) }
}
}

为了方便使用,有些启动器会增加一些更易用的 launch() 方法。比如这个例子能改成下面更简洁的写法。


startActivityLauncher.launch(KEY_NAME to "nickname") { resultCode, data ->
if (resultCode == RESULT_OK) {
data?.getStringExtra(KEY_VALUE)?.let { toast(it) }
}
}

由于输入文字页面可能有多个地方需要跳转复用,我们可以用前面的封装思路,自定义实现一个 InputTextLauncher 类,进一步简化调用的代码,只关心输入值和输出值,不用再处理跳转和解析过程。


inputTextLauncher.launch("nickname") { value ->
if (value != null) {
toast(value)
}
}

通常要对返回值进行判断,因为可能会有取消操作,要判断是不是被取消了。比如返回的 Boolean 要为 true,返回的 Uri 不为 null,返回的数组不为空数组等。


还有一些常用的功能,比如调用系统相机拍照和跳转系统相册选择图片,已适配 Android 10,可以直接得到 uri 来加载图片和用 file 进行上传等操作。


takePictureLauncher.launch { uri, file ->
if (uri != null && file != null) {
// 上传或取消等操作后建议把缓存文件删除,调用 file.delete()
}
}

pickContentLauncher.launchForImage(
onActivityResult = { uri, file ->
if (uri != null && file != null) {
// 上传或取消等操作后建议把缓存文件删除,调用 file.delete()
}
},
onPermissionDenied = {
// 拒绝了读取权限且不再询问,可引导用户到设置里授权该权限
},
onExplainRequestPermission = {
// 拒绝了一次读取权限,可弹框解释为什么要获取该权限
}
)

个人也新增了些功能,比如裁剪图片,通常上传头像要裁剪成 1:1 比例,已适配 Android 11。


cropPictureLauncher.launch(inputUri) { uri, file ->
if (uri != null && file != null) {
// 上传或取消等操作后建议把缓存文件删除,调用 file.delete()
}
}

还有开启蓝牙功能,能更容易地开启蓝牙和确保蓝牙功能是可用的(需要授权定位权限和确保定位已打开)。


enableBluetoothLauncher.launchAndEnableLocation(
"为保证蓝牙正常使用,请开启定位", // 已授权权限但未开启定位,会跳转对应设置页面,并吐司该字符串
onLocationEnabled= { enabled ->
if (enabled) {
// 已开启了蓝牙,并且授权了位置权限和打开了定位
}
},
onPermissionDenied = {
// 拒绝了位置权限且不再询问,可引导用户到设置里授权该权限
},
onExplainRequestPermission = {
// 拒绝了一次位置权限,可弹框解释为什么要获取该权限
}
)

更多的用法请查看 Wiki 文档


原本 Activity Result API 已经有很多默认的协议类,都封装了对应的启动器类。大家可能不会用到所有类,开了混淆会自动移除没使用到的类。


彩蛋


个人之前封装过一个 startActivityForResult() 拓展函数,可以直接在后面写回调逻辑。


startActivityForResult(intent, requestCode) { resultCode, data ->
// Handle result
}

下面是实现的代码,使用一个 Fragment 来分发 onActivityResult 的结果。代码量不多,逻辑应该比较清晰,感兴趣的可以了解一下,Activity Result API 的实现原理应该也是类似的。


inline fun FragmentActivity.startActivityForResult(
intent:
Intent,
requestCode:
Int,
noinline callback: (resultCode: Int, data: Intent?) -> Unit
)
=
DispatchResultFragment.getInstance(this).startActivityForResult(intent, requestCode, callback)

class DispatchResultFragment : Fragment() {
private val callbacks = SparseArray<(resultCode: Int, data: Intent?) -> Unit>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true
}

fun startActivityForResult(
intent:
Intent,
requestCode:
Int,
callback: (
resultCode: Int, data: Intent?) -> Unit
)
{
callbacks.put(requestCode, callback)
startActivityForResult(intent, requestCode)
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
val callback = callbacks.get(requestCode)
if (callback != null) {
callback.invoke(resultCode, data)
callbacks.remove(requestCode)
}
}

companion object {
private const val TAG = "dispatch_result"

fun getInstance(activity: FragmentActivity): DispatchResultFragment =
activity.run {
val fragmentManager = supportFragmentManager
var fragment = fragmentManager.findFragmentByTag(TAG) as DispatchResultFragment?
if (fragment == null) {
fragment = DispatchResultFragment()
fragmentManager.beginTransaction().add(fragment, TAG).commitAllowingStateLoss()
fragmentManager.executePendingTransactions()
}
fragment
}
}
}

如果觉得 Activity Result API 比较复杂,也可以拷贝这个去用。不过 requestCode 处理得不够好,而且很多功能需要自己额外去实现,用起来可能没那么方便。



收起阅读 »

最优解前端面试题答法

1. JS事件冒泡和事件代理(委托) 1. 事件冒泡 会从当前触发的事件目标一级一级往上传递,依次触发,直到document为止。 <body> <div id="parentId"> 查看消息信息 <div id="chi...
继续阅读 »

1. JS事件冒泡和事件代理(委托)


1. 事件冒泡


会从当前触发的事件目标一级一级往上传递,依次触发,直到document为止。


<body>    <div id="parentId"> 查看消息信息 <div id="childId1"> 删除消息信息 </div>    </div></body><script>    let parent = document.getElementById('parentId');    let childId1 = document.getElementById('childId1');    parent.addEventListener('click', function () {        alert('查看消息信息');    }, false);    childId1.addEventListener('click', function () {        alert('删除消息信息');    }, false);     // 如出发消息列表里的删除按钮, 先执行了删除操作, 在向上冒泡执行‘ 查看消息信息’。        // 打印:删除消息信息 查看消息信息</script>

原生js取消事件冒泡


   try{
e.stopPropagation();//非IE浏览器
}
catch(e){
window.event.cancelBubble = true;//IE浏览器
}

vue.js取消事件冒泡


<div @click.stop="doSomething($event)">vue取消事件冒泡</div>

2. 事件代理(委托)


a. 为什么要用事件委托:


比如ul下有100个li,用for循环遍历所有的li,然后给它们添加事件,需要不断的与dom节点进行交互,访问dom的次数越多,引起浏览器重绘与重排的次数也就越多,就会延长整个页面的交互就绪时间,这就是为什么性能优化的主要思想之一就是减少DOM操作的原因;


如果要用事件委托,就会将所有的操作放到js程序里面,与dom的操作就只需要交互一次,这样就能大大的减少与dom的交互次数,提高性能;


b. 事件委托的原理


事件委托:利用事件冒泡的特性,将本应该注册在子元素上的处理事件注册在父元素上,这样点击子元素时发现其本身没有相应事件就到父元素上寻找作出相应。这样做的优势有:


1、减少DOM操作,提高性能。


2、随时可以添加子元素,添加的子元素会自动有相应的处理事件。


<div id="box">
<input type="button" id="add" value="添加" />
<input type="button" id="remove" value="删除" />
<input type="button" id="move" value="移动" />
<input type="button" id="select" value="选择" />
</div>
方式一:需要4次dom操作
window.onload = function () {
var Add = document.getElementById("add");
var Remove = document.getElementById("remove");
var Move = document.getElementById("move");
var Select = document.getElementById("select");
Add.onclick = function () { alert('添加'); }; Remove.onclick = function () { alert('删除'); }; Move.onclick = function () { alert('移动'); }; Select.onclick = function () { alert('选择'); } }
方式二:委托它们父级代为执行事件
window.onload = function(){
var oBox = document.getElementById("box");
oBox.onclick = function (ev) {
var ev = ev || window.event;
var target = ev.target || ev.srcElement;
if(target.nodeName.toLocaleLowerCase() == 'input'){
switch(target.id){
case 'add' :
alert('添加');
break;
case 'remove' :
alert('删除');
break;
case 'move' :
alert('移动');
break;
case 'select' :
alert('选择');
break;
}
}
}

}
用事件委托就可以只用一次dom操作就能完成所有的效果,比上面的性能肯定是要好一些的

3. 事件捕获


会从document开始触发,一级一级往下传递,依次触发,直到真正事件目标为止。


    <div> <button>            <p>点击捕获</p>        </button></div>    <script>        var oP = document.querySelector('p');        var oB = document.querySelector('button');        var oD = document.querySelector('div');        var oBody = document.querySelector('body');        oP.addEventListener('click', function () {            console.log('p标签被点击')        }, true);        oB.addEventListener('click', function () {            console.log("button被点击")        }, true);        oD.addEventListener('click', function () {            console.log('div被点击')        }, true);        oBody.addEventListener('click', function () {            console.log('body被点击')        }, true);    </script>    点击<p>点击捕获</p>,打印的顺序是:body=>div=>button=>p</body>

流程:先捕获,然后处理,然后再冒泡出去。


2. 原型链



1. 原型对象诞生原因和本质


为了解决无法共享公共属性的问题,所以要设计一个对象专门用来存储对象共享的属性,那么我们叫它「原型对象


原理:构造函数加一个属性叫做prototype,用来指向原型对象,我们把所有实例对象共享的属性和方法都放在这个构造函数的prototype属性指向的原型对象中,不需要共享的属性和方法放在构造函数中。实现构造函数生成的所有实例对象都能够共享属性。


构造函数:私有属性
原型对象:共有属性

2.  彼此之间的关系


构造函数中一属性prototype:指向原型对象,而原型对象一constructor属性,又指回了构造函数。

每个构造函数生成的实例对象都有一个proto属性,这个属性指向原型对象。


那原型对象的_proto_属性指向谁?-> null


3. 原型链是什么?


顾名思义,肯定是一条链,既然每个对象都有一个_proto_属性指向原型对象,那么原型对象也有_proto_指向原型对象的原型对象,直到指向上图中的null,这才到达原型链的顶端。


4. 原型链和继承使用场景


原型链主要用于继承,实现代码复用。因为js算不上是面向对象的语言,继承是基于原型实现而不是基于类实现的,


a. 判断函数的原型是否在对象的原型链上


对象 instanceof 函数(不推荐使用)


b. 创建一个新对象,该新对象的隐式原型指向指定的对象


Object.create(对象)


var obj = Object.create(Object.prototype);


obj.__proto__ === Object.prototype


c. new的实现


d. es6的class A extends B 


因为es6-没有类和继承的概念。js实现继承本质是把js中的对象构造函数在自己的脑中抽象成一个类,然后使用构造函数的protptype属性封装出一个类(另一个构造函数),使之完美继承前一构造函数的所有属性和方法。因为构造函数能new出一个具体的对象实例,这就在js中实现了现代化的面向对象和继承。


3. 闭包和垃圾回收机制


闭包的概念


  function f1(){    var n=999;    function f2(){      alert(n);    }    return f2;  }  var result=f1();  result(); // 999

在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。这就是Javascript语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。


既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!


作用:一是前面提到的可以读取函数内部的变量,二是让这些变量的值始终保持在内存中,主要用来封装私有变量, 提供一些暴露的接口


垃圾回收


**垃圾回收机制:JavaScript 引擎中有一个后台进程称为垃圾回收器,它监视所有对象,并删除那些不可访问的对象

**


**注意点:**对于内存的管理,Javascript与C语言等底层语言JavaScript是在创建变量(对象,字符串等)时自动进行了分配内存,并且在不使用它们时“自动”释放。 释放的过程称为垃圾回收。这个“自动”是混乱的根源,并让JavaScript(和其他高级语言)开发者错误的感觉他们可以不关心内存管理,


实现的原理:由于 f2 中引用了 相对于自己的全局变量 n ,所以 f2 会一直存在内存中,又因为 n 是 f1 中的局部变量,也就是说 f2 依赖 f1,所以说 f1 也会一直存在内存中,并不像普通函数那样,调用后变量便被垃圾回收了。


所以说,在setTimeout中的函数引用了外层 for循环的变量 i,导致 i 一直存在内存中,不被回收,所以等到JS队列执行 函数时,i 已经是 10了,所以最终打印 10个10。


五、使用闭包的注意点


1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。


for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
打印:5个6,原因js事件执行机制
办法一:
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})(i);
}
打印:依次输出1到5
原因:因为实际参数跟定时器内部的i有强依赖。通过闭包,将i的变量驻留在内存中,当输出j时,
引用的是外部函数的变量值i,i的值是根据循环来的,执行setTimeout时已经确定了里面的的输出了。办法二:
for (let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
打印:依次输出1到5因为for循环头部的let不仅将i绑定到for循环中,事实上它将其重新绑定到循环体的每一次迭代中,
确保上一次迭代结束的值重新被赋值。
setTimeout里面的function()属于一个新的域,
通过var定义的变量是无法传入到这个函数执行域中的,
通过使用let来声明块变量能作用于这个块,所以function就能使用i这个变量了;
这个匿名函数的参数作用域和for参数的作用域不一样,是利用了这一点来完成的。
这个匿名函数的作用域有点类似类的属性,是可以被内层方法使用的。

4. js事件执行机制


事件循环的过程如下:



  1. JS引擎(唯一主线程)按顺序解析代码,遇到函数声明,直接跳过,遇到函数调用,入栈;

  2. 如果是同步函数调用,直接执行得到结果,同步函数弹出栈,继续下一个函数调用;

  3. 如果是异步函数调用,分发给Web API(多个辅助线程),异步函数弹出栈,继续下一个函数调用;

  4. Web API中,异步函数在相应辅助线程中处理完成后,即异步函数达到触发条件了(比如setTimeout设置的10s后),如果异步函数是宏任务,则入宏任务消息队列,如果是微任务,则入微任务消息队列;

  5. Event Loop不停地检查主线程的调用栈与回调队列,当调用栈空时,就把微任务消息队列中的第一个任务推入栈中执行,执行完成后,再取第二个微任务,直到微任务消息队列为空;然后

    去宏任务消息队列中取第一个宏任务推入栈中执行,当该宏任务执行完成后,在下一个宏任务执行前,再依次取出微任务消息队列中的所有微任务入栈执行。

  6. 上述过程不断循环,每当微任务队列清空,可作为本轮事件循环的结束。




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

收起阅读 »

项目中实用的前端性能优化

一、CDN 1. CDN的概念 CDN(Content Delivery Network,内容分发网络)是指一种通过互联网互相连接的电脑网络系统,利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户,来提供高性能、可扩展性...
继续阅读 »

一、CDN


1. CDN的概念


CDN(Content Delivery Network,内容分发网络)是指一种通过互联网互相连接的电脑网络系统,利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户,来提供高性能、可扩展性及低成本的网络内容传递给用户。


典型的CDN系统由下面三个部分组成:



  • 分发服务系统: 最基本的工作单元就是Cache设备,cache(边缘cache)负责直接响应最终用户的访问请求,把缓存在本地的内容快速地提供给用户。同时cache还负责与源站点进行内容同步,把更新的内容以及本地没有的内容从源站点获取并保存在本地。Cache设备的数量、规模、总服务能力是衡量一个CDN系统服务能力的最基本的指标。

  • 负载均衡系统: 主要功能是负责对所有发起服务请求的用户进行访问调度,确定提供给用户的最终实际访问地址。两级调度体系分为全局负载均衡(GSLB)和本地负载均衡(SLB)。全局负载均衡主要根据用户就近性原则,通过对每个服务节点进行“最优”判断,确定向用户提供服务的cache的物理位置。本地负载均衡主要负责节点内部的设备负载均衡

  • **运营管理系统:**运营管理系统分为运营管理和网络管理子系统,负责处理业务层面的与外界系统交互所必须的收集、整理、交付工作,包含客户管理、产品管理、计费管理、统计分析等功能。


2. CDN的作用


CDN一般会用来托管Web资源(包括文本、图片和脚本等),可供下载的资源(媒体文件、软件、文档等),应用程序(门户网站等)。使用CDN来加速这些资源的访问。


(1)在性能方面,引入CDN的作用在于:



  • 用户收到的内容来自最近的数据中心,延迟更低,内容加载更快

  • 部分资源请求分配给了CDN,减少了服务器的负载


(2)在安全方面,CDN有助于防御DDoS、MITM等网络攻击:



  • 针对DDoS:通过监控分析异常流量,限制其请求频率

  • 针对MITM:从源服务器到 CDN 节点到 ISP(Internet Service Provider),全链路 HTTPS 通信


除此之外,CDN作为一种基础的云服务,同样具有资源托管、按需扩展(能够应对流量高峰)等方面的优势。


3. CDN的原理


CDN和DNS有着密不可分的联系,先来看一下DNS的解析域名过程,在浏览器输入 http://www.test.com 的解析过程如下:


(1) 检查浏览器缓存


(2)检查操作系统缓存,常见的如hosts文件


(3)检查路由器缓存


(4)如果前几步都没没找到,会向ISP(网络服务提供商)的LDNS服务器查询


(5)如果LDNS服务器没找到,会向根域名服务器(Root Server)请求解析,分为以下几步:



  • 根服务器返回顶级域名(TLD)服务器如.com.cn.org等的地址,该例子中会返回.com的地址

  • 接着向顶级域名服务器发送请求,然后会返回次级域名(SLD)服务器的地址,本例子会返回.test的地址

  • 接着向次级域名服务器发送请求,然后会返回通过域名查询到的目标IP,本例子会返回http://www.test.com的地址

  • Local DNS Server会缓存结果,并返回给用户,缓存在系统中


CDN的工作原理:


(1)用户未使用CDN缓存资源的过程:



  1. 浏览器通过DNS对域名进行解析(就是上面的DNS解析过程),依次得到此域名对应的IP地址

  2. 浏览器根据得到的IP地址,向域名的服务主机发送数据请求

  3. 服务器向浏览器返回响应数据


(2)用户使用CDN缓存资源的过程:



  1. 对于点击的数据的URL,经过本地DNS系统的解析,发现该URL对应的是一个CDN专用的DNS服务器,DNS系统就会将域名解析权交给CNAME指向的CDN专用的DNS服务器。

  2. CND专用DNS服务器将CND的全局负载均衡设备IP地址返回给用户

  3. 用户向CDN的全局负载均衡设备发起数据请求

  4. CDN的全局负载均衡设备根据用户的IP地址,以及用户请求的内容URL,选择一台用户所属区域的区域负载均衡设备,告诉用户向这台设备发起请求

  5. 区域负载均衡设备选择一台合适的缓存服务器来提供服务,将该缓存服务器的IP地址返回给全局负载均衡设备

  6. 全局负载均衡设备把服务器的IP地址返回给用户

  7. 用户向该缓存服务器发起请求,缓存服务器响应用户的请求,将用户所需内容发送至用户终端。


如果缓存服务器没有用户想要的内容,那么缓存服务器就会向它的上一级缓存服务器请求内容,以此类推,直到获取到需要的资源。最后如果还是没有,就会回到自己的服务器去获取资源。


image


CNAME(意为:别名):在域名解析中,实际上解析出来的指定域名对应的IP地址,或者该域名的一个CNAME,然后再根据这个CNAME来查找对应的IP地址。


4. CDN的使用场景



  • **使用第三方的CDN服务:**如果想要开源一些项目,可以使用第三方的CDN服务

  • **使用CDN进行静态资源的缓存:**将自己网站的静态资源放在CDN上,比如js、css、图片等。可以将整个项目放在CDN上,完成一键部署。

  • **直播传送:**直播本质上是使用流媒体进行传送,CDN也是支持流媒体传送的,所以直播完全可以使用CDN来提高访问速度。CDN在处理流媒体的时候与处理普通静态文件有所不同,普通文件如果在边缘节点没有找到的话,就会去上一层接着寻找,但是流媒体本身数据量就非常大,如果使用回源的方式,必然会带来性能问题,所以流媒体一般采用的都是主动推送的方式来进行。


二、懒加载


1. 懒加载的概念


懒加载也叫做延迟加载、按需加载,指的是在长网页中延迟加载图片数据,是一种较好的网页性能优化的方式。在比较长的网页或应用中,如果图片很多,所有的图片都被加载出来,而用户只能看到可视窗口的那一部分图片数据,这样就浪费了性能。


如果使用图片的懒加载就可以解决以上问题。在滚动屏幕之前,可视化区域之外的图片不会进行加载,在滚动屏幕时才加载。这样使得网页的加载速度更快,减少了服务器的负载。懒加载适用于图片较多,页面列表较长(长列表)的场景中。


2. 懒加载的特点



  • 减少无用资源的加载:使用懒加载明显减少了服务器的压力和流量,同时也减小了浏览器的负担。

  • 提升用户体验: 如果同时加载较多图片,可能需要等待的时间较长,这样影响了用户体验,而使用懒加载就能大大的提高用户体验。

  • 防止加载过多图片而影响其他资源文件的加载 :会影响网站应用的正常使用。


3. 懒加载的实现原理


图片的加载是由src引起的,当对src赋值时,浏览器就会请求图片资源。根据这个原理,我们使用HTML5 的data-xxx属性来储存图片的路径,在需要加载图片的时候,将data-xxx中图片的路径赋值给src,这样就实现了图片的按需加载,即懒加载。


注意:data-xxx 中的xxx可以自定义,这里我们使用data-src来定义。


懒加载的实现重点在于确定用户需要加载哪张图片,在浏览器中,可视区域内的资源就是用户需要的资源。所以当图片出现在可视区域时,获取图片的真实地址并赋值给图片即可。


使用原生JavaScript实现懒加载:


知识点:


(1)window.innerHeight 是浏览器可视区的高度


(2)document.body.scrollTop || document.documentElement.scrollTop 是浏览器滚动的过的距离


(3)imgs.offsetTop 是元素顶部距离文档顶部的高度(包括滚动条的距离)


(4)图片加载条件:img.offsetTop < window.innerHeight + document.body.scrollTop;


图示:


image


代码实现:


<div>
<img src="loading.gif" data-src="pic.png">
<img src="loading.gif" data-src="pic.png">
<img src="loading.gif" data-src="pic.png">
<img src="loading.gif" data-src="pic.png">
<img src="loading.gif" data-src="pic.png">
<img src="loading.gif" data-src="pic.png">
</div>
<script>
var imgs = document.querySelectorAll('img');
function lozyLoad(){
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
var winHeight= window.innerHeight;
for(var i=0;i < imgs.length;i++){
if(imgs[i].offsetTop < scrollTop + winHeight ){
imgs[i].src = imgs[i].getAttribute('data-src');
}
}
}
window.onscroll = lozyLoad;
</script>

4. 懒加载与预加载的区别


这两种方式都是提高网页性能的方式,两者主要区别是一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。



  • 懒加载也叫延迟加载,指的是在长网页中延迟加载图片的时机,当用户需要访问时,再去加载,这样可以提高网站的首屏加载速度,提升用户的体验,并且可以减少服务器的压力。它适用于图片很多,页面很长的电商网站的场景。懒加载的实现原理是,将页面上的图片的 src 属性设置为空字符串,将图片的真实路径保存在一个自定义属性中,当页面滚动的时候,进行判断,如果图片进入页面可视区域内,则从自定义属性中取出真实路径赋值给图片的 src 属性,以此来实现图片的延迟加载。

  • 预加载指的是将所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存取资源。 通过预加载能够减少用户的等待时间,提高用户的体验。我了解的预加载的最常用的方式是使用 js 中的 image 对象,通过为 image 对象来设置 scr 属性,来实现图片的预加载。


三、回流与重绘


1. 回流与重绘的概念及触发条件


(1)回流


当渲染树中部分或者全部元素的尺寸、结构或者属性发生变化时,浏览器会重新渲染部分或者全部文档的过程就称为回流


下面这些操作会导致回流:



  • 页面的首次渲染

  • 浏览器的窗口大小发生变化

  • 元素的内容发生变化

  • 元素的尺寸或者位置发生变化

  • 元素的字体大小发生变化

  • 激活CSS伪类

  • 查询某些属性或者调用某些方法

  • 添加或者删除可见的DOM元素


在触发回流(重排)的时候,由于浏览器渲染页面是基于流式布局的,所以当触发回流时,会导致周围的DOM元素重新排列,它的影响范围有两种:



  • 全局范围:从根节点开始,对整个渲染树进行重新布局

  • 局部范围:对渲染树的某部分或者一个渲染对象进行重新布局


(2)重绘


当页面中某些元素的样式发生变化,但是不会影响其在文档流中的位置时,浏览器就会对元素进行重新绘制,这个过程就是重绘


下面这些操作会导致回流:



  • color、background 相关属性:background-color、background-image 等

  • outline 相关属性:outline-color、outline-width 、text-decoration

  • border-radius、visibility、box-shadow


注意: 当触发回流时,一定会触发重绘,但是重绘不一定会引发回流。


2. 如何避免回流与重绘?


减少回流与重绘的措施:



  • 操作DOM时,尽量在低层级的DOM节点进行操作

  • 不要使用table布局, 一个小的改动可能会使整个table进行重新布局

  • 使用CSS的表达式

  • 不要频繁操作元素的样式,对于静态页面,可以修改类名,而不是样式。

  • 使用absolute或者fixed,使元素脱离文档流,这样他们发生变化就不会影响其他元素

  • 避免频繁操作DOM,可以创建一个文档片段documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中

  • 将元素先设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。

  • 将DOM的多个读操作(或者写操作)放在一起,而不是读写操作穿插着写。这得益于浏览器的渲染队列机制


浏览器针对页面的回流与重绘,进行了自身的优化——渲染队列


浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行批处理。这样就会让多次的回流、重绘变成一次回流重绘。


上面,将多个读操作(或者写操作)放在一起,就会等所有的读操作进入队列之后执行,这样,原本应该是触发多次回流,变成了只触发一次回流。


3. 如何优化动画?


对于如何优化动画,我们知道,一般情况下,动画需要频繁的操作DOM,就就会导致页面的性能问题,我们可以将动画的position属性设置为absolute或者fixed,将动画脱离文档流,这样他的回流就不会影响到页面了。


4. documentFragment 是什么?用它跟直接操作 DOM 的区别是什么?


MDN中对documentFragment的解释:



DocumentFragment,文档片段接口,一个没有父对象的最小文档对象。它被作为一个轻量版的 Document使用,就像标准的document一样,存储由节点(nodes)组成的文档结构。与document相比,最大的区别是DocumentFragment不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会导致性能等问题。



当我们把一个 DocumentFragment 节点插入文档树时,插入的不是 DocumentFragment 自身,而是它的所有子孙节点。在频繁的DOM操作时,我们就可以将DOM元素插入DocumentFragment,之后一次性的将所有的子孙节点插入文档中。和直接操作DOM相比,将DocumentFragment 节点插入DOM树时,不会触发页面的重绘,这样就大大提高了页面的性能。


四、节流与防抖


1. 对节流与防抖的理解



  • 函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。

  • 函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在 scroll 函数的事件监听上,通过事件节流来降低事件调用的频率。


防抖函数的应用场景:



  • 按钮提交场景:防⽌多次提交按钮,只执⾏最后提交的⼀次

  • 服务端验证场景:表单验证需要服务端配合,只执⾏⼀段连续的输⼊事件的最后⼀次,还有搜索联想词功能类似⽣存环境请⽤lodash.debounce


节流函数的****适⽤场景:



  • 拖拽场景:固定时间内只执⾏⼀次,防⽌超⾼频次触发位置变动

  • 缩放场景:监控浏览器resize

  • 动画场景:避免短时间内多次触发动画引起性能问题


2. 实现节流函数和防抖函数


函数防抖的实现:


function debounce(fn, wait) {
var timer = null;

return function() {
var context = this,
args = [...arguments];

// 如果此时存在定时器的话,则取消之前的定时器重新记时
if (timer) {
clearTimeout(timer);
timer = null;
}

// 设置定时器,使事件间隔指定事件后执行
timer = setTimeout(() => {
fn.apply(context, args);
}, wait);
};
}

函数节流的实现:


// 时间戳版
function throttle(fn, delay) {
var preTime = Date.now();

return function() {
var context = this,
args = [...arguments],
nowTime = Date.now();

// 如果两次时间间隔超过了指定时间,则执行函数。
if (nowTime - preTime >= delay) {
preTime = Date.now();
return fn.apply(context, args);
}
};
}

// 定时器版
function throttle (fun, wait){
let timeout = null
return function(){
let context = this
let args = [...arguments]
if(!timeout){
timeout = setTimeout(() => {
fun.apply(context, args)
timeout = null
}, wait)
}
}
}

五、图片优化


1. 如何对项目中的图片进行优化?



  1. 不用图片。很多时候会使用到很多修饰类图片,其实这类修饰图片完全可以用 CSS 去代替。

  2. 对于移动端来说,屏幕宽度就那么点,完全没有必要去加载原图浪费带宽。一般图片都用 CDN 加载,可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图片。

  3. 小图使用 base64 格式

  4. 将多个图标文件整合到一张图片中(雪碧图)

  5. 选择正确的图片格式:





    • 对于能够显示 WebP 格式的浏览器尽量使用 WebP 格式。因为 WebP 格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好

    • 小图使用 PNG,其实对于大部分图标这类图片,完全可以使用 SVG 代替

    • 照片使用 JPEG




2. 常见的图片格式及使用场景


(1)BMP,是无损的、既支持索引色也支持直接色的点阵图。这种图片格式几乎没有对数据进行压缩,所以BMP格式的图片通常是较大的文件。


(2)GIF是无损的、采用索引色的点阵图。采用LZW压缩算法进行编码。文件小,是GIF格式的优点,同时,GIF格式还具有支持动画以及透明的优点。但是GIF格式仅支持8bit的索引色,所以GIF格式适用于对色彩要求不高同时需要文件体积较小的场景。


(3)JPEG是有损的、采用直接色的点阵图。JPEG的图片的优点是采用了直接色,得益于更丰富的色彩,JPEG非常适合用来存储照片,与GIF相比,JPEG不适合用来存储企业Logo、线框类的图。因为有损压缩会导致图片模糊,而直接色的选用,又会导致图片文件较GIF更大。


(4)PNG-8是无损的、使用索引色的点阵图。PNG是一种比较新的图片格式,PNG-8是非常好的GIF格式替代者,在可能的情况下,应该尽可能的使用PNG-8而不是GIF,因为在相同的图片效果下,PNG-8具有更小的文件体积。除此之外,PNG-8还支持透明度的调节,而GIF并不支持。除非需要动画的支持,否则没有理由使用GIF而不是PNG-8。


(5)PNG-24是无损的、使用直接色的点阵图。PNG-24的优点在于它压缩了图片的数据,使得同样效果的图片,PNG-24格式的文件大小要比BMP小得多。当然,PNG24的图片还是要比JPEG、GIF、PNG-8大得多。


(6)SVG是无损的矢量图。SVG是矢量图意味着SVG图片由直线和曲线以及绘制它们的方法组成。当放大SVG图片时,看到的还是线和曲线,而不会出现像素点。这意味着SVG图片在放大时,不会失真,所以它非常适合用来绘制Logo、Icon等。


(7)WebP是谷歌开发的一种新图片格式,WebP是同时支持有损和无损压缩的、使用直接色的点阵图。从名字就可以看出来它是为Web而生的,什么叫为Web而生呢?就是说相同质量的图片,WebP具有更小的文件体积。现在网站上充满了大量的图片,如果能够降低每一个图片的文件大小,那么将大大减少浏览器和服务器之间的数据传输量,进而降低访问延迟,提升访问体验。目前只有Chrome浏览器和Opera浏览器支持WebP格式,兼容性不太好。



  • 在无损压缩的情况下,相同质量的WebP图片,文件大小要比PNG小26%;

  • 在有损压缩的情况下,具有相同图片精度的WebP图片,文件大小要比JPEG小25%~34%;

  • WebP图片格式支持图片透明度,一个无损压缩的WebP图片,如果要支持透明度只需要22%的格外文件大小。


六、Webpack优化


1. 如何提⾼webpack的打包速度**?**


(1)优化 Loader


对于 Loader 来说,影响打包效率首当其冲必属 Babel 了。因为 Babel 会将代码转为字符串生成 AST,然后对 AST 继续进行转变最后再生成新的代码,项目越大,转换代码越多,效率就越低。当然了,这是可以优化的。


首先我们优化 Loader 的文件搜索范围


module.exports = {
module: {
rules: [
{
// js 文件才使用 babel
test: /\.js$/,
loader: 'babel-loader',
// 只在 src 文件夹下查找
include: [resolve('src')],
// 不会去查找的路径
exclude: /node_modules/
}
]
}
}

对于 Babel 来说,希望只作用在 JS 代码上的,然后 node_modules 中使用的代码都是编译过的,所以完全没有必要再去处理一遍。


当然这样做还不够,还可以将 Babel 编译过的文件缓存起来,下次只需要编译更改过的代码文件即可,这样可以大幅度加快打包时间


loader: 'babel-loader?cacheDirectory=true'

(2)HappyPack


受限于 Node 是单线程运行的,所以 Webpack 在打包的过程中也是单线程的,特别是在执行 Loader 的时候,长时间编译的任务很多,这样就会导致等待的情况。


HappyPack 可以将 Loader 的同步执行转换为并行的,这样就能充分利用系统资源来加快打包效率了


module: {
loaders: [
{
test: /\.js$/,
include: [resolve('src')],
exclude: /node_modules/,
// id 后面的内容对应下面
loader: 'happypack/loader?id=happybabel'
}
]
},
plugins: [
new HappyPack({
id: 'happybabel',
loaders: ['babel-loader?cacheDirectory'],
// 开启 4 个线程
threads: 4
})
]

(3)DllPlugin


DllPlugin 可以将特定的类库提前打包然后引入。这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。DllPlugin的使用方法如下:


// 单独配置在一个文件中
// webpack.dll.conf.js
const path = require('path')
const webpack = require('webpack')
module.exports = {
entry: {
// 想统一打包的类库
vendor: ['react']
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].dll.js',
library: '[name]-[hash]'
},
plugins: [
new webpack.DllPlugin({
// name 必须和 output.library 一致
name: '[name]-[hash]',
// 该属性需要与 DllReferencePlugin 中一致
context: __dirname,
path: path.join(__dirname, 'dist', '[name]-manifest.json')
})
]
}

然后需要执行这个配置文件生成依赖文件,接下来需要使用 DllReferencePlugin 将依赖文件引入项目中


// webpack.conf.js
module.exports = {
// ...省略其他配置
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
// manifest 就是之前打包出来的 json 文件
manifest: require('./dist/vendor-manifest.json'),
})
]
}

(4)代码压缩


在 Webpack3 中,一般使用 UglifyJS 来压缩代码,但是这个是单线程运行的,为了加快效率,可以使用 webpack-parallel-uglify-plugin 来并行运行 UglifyJS,从而提高效率。


在 Webpack4 中,不需要以上这些操作了,只需要将 mode 设置为 production 就可以默认开启以上功能。代码压缩也是我们必做的性能优化方案,当然我们不止可以压缩 JS 代码,还可以压缩 HTML、CSS 代码,并且在压缩 JS 代码的过程中,我们还可以通过配置实现比如删除 console.log 这类代码的功能。


(5)其他


可以通过一些小的优化点来加快打包速度



  • resolve.extensions:用来表明文件后缀列表,默认查找顺序是 ['.js', '.json'],如果你的导入文件没有添加后缀就会按照这个顺序查找文件。我们应该尽可能减少后缀列表长度,然后将出现频率高的后缀排在前面

  • resolve.alias:可以通过别名的方式来映射一个路径,能让 Webpack 更快找到路径

  • module.noParse:如果你确定一个文件下没有其他依赖,就可以使用该属性让 Webpack 不扫描该文件,这种方式对于大型的类库很有帮助


2. 如何减少 Webpack 打包体积


(1)按需加载


在开发 SPA 项目的时候,项目中都会存在很多路由页面。如果将这些页面全部打包进一个 JS 文件的话,虽然将多个请求合并了,但是同样也加载了很多并不需要的代码,耗费了更长的时间。那么为了首页能更快地呈现给用户,希望首页能加载的文件体积越小越好,这时候就可以使用按需加载,将每个路由页面单独打包为一个文件。当然不仅仅路由可以按需加载,对于 loadash 这种大型类库同样可以使用这个功能。


按需加载的代码实现这里就不详细展开了,因为鉴于用的框架不同,实现起来都是不一样的。当然了,虽然他们的用法可能不同,但是底层的机制都是一样的。都是当使用的时候再去下载对应文件,返回一个 Promise,当 Promise 成功以后去执行回调。


(2)Scope Hoisting


Scope Hoisting 会分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中去。


比如希望打包两个文件:


// test.js
export const a = 1
// index.js
import { a } from './test.js'

对于这种情况,打包出来的代码会类似这样:


[
/* 0 */
function (module, exports, require) {
//...
},
/* 1 */
function (module, exports, require) {
//...
}
]

但是如果使用 Scope Hoisting ,代码就会尽可能的合并到一个函数中去,也就变成了这样的类似代码:


[
/* 0 */
function (module, exports, require) {
//...
}
]

这样的打包方式生成的代码明显比之前的少多了。如果在 Webpack4 中你希望开启这个功能,只需要启用 optimization.concatenateModules 就可以了:


module.exports = {
optimization: {
concatenateModules: true
}
}

(3)Tree Shaking


Tree Shaking 可以实现删除项目中未被引用的代码,比如:


// test.js
export const a = 1
export const b = 2
// index.js
import { a } from './test.js'

对于以上情况,test 文件中的变量 b 如果没有在项目中使用到的话,就不会被打包到文件中。


如果使用 Webpack 4 的话,开启生产环境就会自动启动这个优化功能。


3. 如何⽤webpack来优化前端性能?


⽤webpack优化前端性能是指优化webpack的输出结果,让打包的最终结果在浏览器运⾏快速⾼效。



  • 压缩代码:删除多余的代码、注释、简化代码的写法等等⽅式。可以利⽤webpack的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩JS⽂件, 利⽤ cssnano (css-loader?minimize)来压缩css

  • 利⽤CDN加速: 在构建过程中,将引⽤的静态资源路径修改为CDN上对应的路径。可以利⽤webpack对于 output 参数和各loader的 publicPath 参数来修改资源路径

  • Tree Shaking: 将代码中永远不会⾛到的⽚段删除掉。可以通过在启动webpack时追加参数 --optimize-minimize 来实现

  • Code Splitting: 将代码按路由维度或者组件分块(chunk),这样做到按需加载,同时可以充分利⽤浏览器缓存

  • 提取公共第三⽅库: SplitChunksPlugin插件来进⾏公共模块抽取,利⽤浏览器缓存可以⻓期缓存这些⽆需频繁变动的公共代码


4. 如何提⾼webpack的构建速度?



  1. 多⼊⼝情况下,使⽤ CommonsChunkPlugin 来提取公共代码

  2. 通过 externals 配置来提取常⽤库

  3. 利⽤ DllPlugin 和 DllReferencePlugin 预编译资源模块 通过 DllPlugin 来对那些我们引⽤但是绝对不会修改的npm包来进⾏预编译,再通过 DllReferencePlugin 将预编译的模块加载进来。

  4. 使⽤ Happypack 实现多线程加速编译

  5. 使⽤ webpack-uglify-parallel 来提升 uglifyPlugin 的压缩速度。 原理上 webpack-uglify-parallel 采⽤了多核并⾏压缩来提升压缩速度

  6. 使⽤ Tree-shaking 和 Scope Hoisting 来剔除多余代码

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

收起阅读 »

『前端BUG』—— 本地代理导致会话cookie中的数据丢失

vue
问题在本地用代理请求服务端接口,解决跨域问题后,发生了一件极其诡异的事情,明明登录成功了,但是请求每个接口都返回未登录的报错信息。原因该套系统是采用会话cookie进行登录用户的身份认证,故查看每个请求的Request Headers中的cookie的值,发现...
继续阅读 »

问题

在本地用代理请求服务端接口,解决跨域问题后,发生了一件极其诡异的事情,明明登录成功了,但是请求每个接口都返回未登录的报错信息。

原因

该套系统是采用会话cookie进行登录用户的身份认证,故查看每个请求的Request Headers中的cookie的值,发现原本如下图中的红框区域的SESSION不见了。

image.png

而明明登录接口的Response Headers中是存在set-cookie。

image.png

set-cookie会是把其值中的SESSION存储到浏览器的cookie中,存储成功后,每次请求服务端时,都会去浏览器中的cookie中读取SESSION,然后通过Request Headers中的cookie传递到服务端,完成身份认证。

另外set-cookie的值是服务端设置的,我们来认真观察一下set-cookie的值

SESSION=NjE1MTNmZWI1N2ExNDYyZGE4MWE0YmZjNjgwMmFmZGY=; Path=/api/operation/; HttpOnly; SameSite=Lax
复制代码

里面除SESSION,还有PathHttpOnlySameSite,而Path就是导致SESSION无法存储到客户端中的元凶,其中Path的值/api/operation/表示该cookie只有在用请求路径的前缀为/api/operation/才能使用。

回到代理配置中一看,

proxy: getProxy({
'/dev': {
target: 'https://xxx.xxx.com',
pathRewrite: { '^/dev': '/api' },
secure: false,
changeOrigin: true
}
}),
复制代码

代理后,请求服务端的地址为xxx.xxx.com/dev/operati… ,其路径为 dev/operation/xxx,自然与/api/operation/不匹配,导致该cookie无法使用,自然无法将SESSION保存到浏览器的cookie中。

解决

找到原因了,问题很好解决,只要更改一下代理配置。

proxy: getProxy({
'/api': {
target: 'https://xxx.xxx.com',
pathRewrite: { '^/api': '/api' },
secure: false,
changeOrigin: true
}
}),
复制代码

此外不要忘记更改 axios 的配置中的baseURL,将其改为/api/


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

收起阅读 »

「自我检验」输入URL发生了啥?希望你顺便懂这15个知识点

输入URL发生了啥? 1、浏览器的地址栏输入URL并按下回车。 2、浏览器查找当前URL是否存在缓存,并比较缓存是否过期。 3、DNS解析URL对应的IP。 4、根据IP建立TCP连接(三次握手)。 5、HTTP发起请求。 6、服务器处理请求,浏览器接收HT...
继续阅读 »

输入URL发生了啥?



  • 1、浏览器的地址栏输入URL并按下回车。

  • 2、浏览器查找当前URL是否存在缓存,并比较缓存是否过期。

  • 3、DNS解析URL对应的IP。

  • 4、根据IP建立TCP连接(三次握手)。

  • 5、HTTP发起请求。

  • 6、服务器处理请求,浏览器接收HTTP响应。

  • 7、渲染页面,构建DOM树。

  • 8、关闭TCP连接(四次挥手)。


永恒钻石


1. 浏览器应该具备什么功能?



  • 1、网络:浏览器通过网络模块来下载各式各样的资源,例如HTML文本,JavaScript代码,CSS样式表,图片,音视频文件等。网络部分尤为重要,因为它耗时长,而且需要安全访问互联网上的资源

  • 2、资源管理:从网络下载,或者本地获取到的资源需要有高效的机制来管理他们。例如如何避免重复下载,资源如何缓存等等

  • 3、网页浏览:这是浏览器的核心也是最基本的功能,最重要的功能。这个功能决定了如何将资源转变为可视化的结果

  • 4、多页面管理

  • 5、插件与管理

  • 6、账户和同步

  • 7、安全机制

  • 8、开发者工具


浏览器的主要功能总结起来就是一句话:将用户输入的url转变成可视化的图像


2. 浏览器的内核


在浏览器中有一个最重要的模块,它主要的作用是把一切请求回来的资源变成可视化的图像,这个模块就是浏览器内核,通常他也被称为渲染引擎


下面是浏览器内核的总结:



  • 1、IE:Trident

  • 2、Safari:WebKit。WebKit本身主要是由两个小引擎构成的,一个正是渲染引擎“WebCore”,另一个则是javascript解释引擎“JSCore”,它们均是从KDE的渲染引擎KHTML及javascript解释引擎KJS衍生而来。

  • 3、Chrome:Blink。在13年发布的Chrome 28.0.1469.0版本开始,Chrome放弃Chromium引擎转而使用最新的Blink引擎(基于WebKit2——苹果公司于2010年推出的新的WebKit引擎),Blink对比上一代的引擎精简了代码、改善了DOM框架,也提升了安全性。

  • 4、Opera:2013年2月宣布放弃Presto,采用Chromium引擎,又转为Blink引擎

  • 5、Firefox:Gecko


3. 进程和线程



  • 1、进程:程序的一次执行,它占有一片独有的内存空间,是操作系统执行的基本单元

  • 2、线程:是进程内的一个独立执行单元,是CPU调度的最小单元,程序运行基本单元

  • 3、一个进程中至少有一个运行的线程:主线程。它在进程启动后自动创建

  • 4、一个进程可以同时运行多个线程,我们常说程序是多线程运行的,比如你使用听歌软件,这个软件就是一个进程,而你在这个软件里听歌收藏歌点赞评论,这就是一个进程里的多个线程操作

  • 5、一个进程中的数据可以供其中的多个线程直接共享,但是进程与进程之间的数据时不能共享

  • 6、JS引擎是单线程运行


4. 浏览器渲染引擎的主要模块



  • 1、HTML解析器:解释HTML文档的解析器,主要作用是将HTML文本解释为DOM树

  • 2、CSS解析器:它的作用是为DOM中的各个元素对象计算出样式信息,为布局提供基础设施

  • 3、JavaScript引擎:JavaScript引擎能够解释JavaScript代码,并通过DOM接口和CSS接口来修改网页内容 和样式信息,从而改变渲染的结果

  • 4、布局(layout):在DOM创建之后,WebKit需要将其中的元素对象同样式信息结合起来,计算他们的大小位置等布局信息,形成一个能表达着所有信息的内部表示模型

  • 5、绘图模块(paint):使用图形库将布局计算后的各个网页的节点绘制成图像结果


5. 大致的渲染过程


第1题的第7点,渲染页面,构建DOM树,接下来说说大致的渲染过程



  • 1、浏览器会从上到下解析文档

  • 2、遇见HTML标记,调用HTML解析器解析为对应的token(一个token就是一个标签文本的序列化)并构建DOM树(就是一块内存,保存着tokens,建立他们之间的关系)

  • 3、遇见style/link标记调用相应解析器处理CSS标记,并构建出CSS样式树

  • 4、遇见script标记,调用JavaScript引擎处理script标记,绑定事件,修改DOM树/CSS树等

  • 5、将DOM树与CSS合并成一个渲染树

  • 6、根据渲染树来渲染,以计算每个节点的几何信息(这一过程需要依赖GPU)

  • 7、最终将各个节点绘制在屏幕上


02_浏览器渲染过程的副本.png


至尊星耀


6. CSS阻塞情况以及优化



  • 1、style标签中的样式:由HTML解析器进行解析,不会阻塞浏览器渲染(可能会产生“闪屏现象”),不会阻塞DOM解析

  • 2、link引入的CSS样式:由CSS解析器进行解析,阻塞浏览器渲染,会阻塞后面的js语句执行,不阻塞DOM的解析

  • 3、优化:使用CDN节点进行外部资源加速,对CSS进行压缩,优化CSS代码(不要使用太多层选择器)


注意:看下图,HTMLCSS是并行解析的,所以CSS不会阻塞HTML解析,但是,会阻塞整体页面的渲染(因为最后要渲染必须CSS和HTML一起解析完并合成一处)
02_浏览器渲染过程的副本.png


7. JS阻塞问题



  • 1、js会阻塞后续DOM的解析,原因是:浏览器不知道后续脚本的内容,如果先去解析了下面的DOM,而随后的js删除了后面所有的DOM,那么浏览器就做了无用功,浏览器无法预估脚本里面具体做了什么操作,例如像document.write这种操作,索性全部停住,等脚本执行完了,浏览器再继续向下解析DOM

  • 2、js会阻塞页面渲染,原因是:js中也可以给DOM设置样式,浏览器等该脚本执行完毕,渲染出一个最终结果,避免做无用功。

  • 3、js会阻塞后续js的执行,原因是维护依赖关系,例如:必须先引入jQuery再引入bootstrap


8. 资源加载阻塞


无论css阻塞,还是js阻塞,都不会阻塞浏览器加载外部资源(图片、视频、样式、脚本等)


原因:浏览器始终处于一种:“先把请求发出去”的工作模式,只要是涉及到网络请求的内容,无论是:图片、样式、脚本,都会先发送请求去获取资源,至于资源到本地之后什么时候用,由浏览器自己协调。这种做法效率很高。


9. 为什么CSS解析顺序从右到左


如果是从左到右的话:



  • 1、第一次从爷节点 -> 子节点 -> 孙节点1

  • 2、第一次从爷节点 -> 子节点 -> 孙节点2

  • 3、第一次从爷节点 -> 子节点 -> 孙节点3


如果三次都匹配不到的话,那至少也得走三次:爷节点 -> 子节点 -> 孙节点,这就做了很多无用功啊。


截屏2021-07-18 下午9.33.13.png


如果是从右到左的话:



  • 1、第一次从孙节点1,找不到,停止

  • 2、第一次从孙节点2,找不到,停止

  • 3、第一次从孙节点3,找不到,停止


这样的话,尽早发现找不到,尽早停止,可以少了很多无用功。


截屏2021-07-18 下午9.37.16.png


最强王者


10. 什么是重绘回流



  • 1、重绘:重绘是一个元素外观的改变所触发的浏览器行为,例如改变outline、背景色等属性。浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。重绘不会带来重新布局,所以并不一定伴随重排。

  • 2、回流:渲染对象在创建完成并添加到渲染树时,并不包含位置和大小信息。计算这些值的过程称为布局或重排,或回流

  • 3、"重绘"不一定需要"重排",比如改变某个网页元素的颜色,就只会触发"重绘",不会触发"重排",因为布局没有改变。

  • 4、"重排"大多数情况下会导致"重绘",比如改变一个网页元素的位置,就会同时触发"重排"和"重绘",因为布局改变了。


11. 触发重绘的属性


* color * background * outline-color * border-style * background-image * outline * border-radius * background-position * outline-style * visibility * background-repeat * outline-width * text-decoration * background-size * box-shadow


12. 触发回流的属性


* width * top * text-align * height * bottom * overflow-y * padding * left * font-weight * margin * right * overflow * display * position * font-family * border-width * float * line-height * border * clear * vertival-align * min-height * white-space


13. 常见触发重绘回流的行为



  • 1、当你增加、删除、修改 DOM 结点时,会导致 Reflow , Repaint。

  • 2、当你移动 DOM 的位置

  • 3、当你修改 CSS 样式的时候。

  • 4、当你Resize窗口的时候(移动端没有这个问题,因为移动端的缩放没有影响布局视口)

  • 5、当你修改网页的默认字体时。

  • 6、获取DOM的height或者width时,例如clientWidth、clientHeight、clientTop、clientLeft、offsetWidth、offsetHeight、offsetTop、offsetLeft、scrollWidth、scrollHeight、scrollTop、scrollLeft、scrollIntoView()、scrollIntoViewIfNeeded()、getComputedStyle()、getBoundingClientRect()、scrollTo()


14. 针对重绘回流的优化方案



  • 1、元素位置移动变换时尽量使用CSS3的transform来代替top,left等操作

  • 2、不要使用table布局

  • 3、将多次改变样式属性的操作合并成一次操作

  • 4、利用文档素碎片(documentFragment),vue使用了该方式提升性能

  • 5、动画实现过程中,启用GPU硬件加速:transform:tranlateZ(0)

  • 6、为动画元素新建图层,提高动画元素的z-index

  • 7、编写动画时,尽量使用requestAnimationFrame


15. 浏览器缓存分类


image.png



  1. 强缓存

    1. 不会向服务器发送请求,直接从本地缓存中获取数据

    2. 请求资源的的状态码为: 200 ok(from memory cache)

    3. 优先级:cache-control > expires



  2. 协商缓存

    1. 向服务器发送请求,服务器会根据请求头的资源判断是否命中协商缓存

    2. 如果命中,则返回304状态码通知浏览器从缓存中读取资源

    3. 优先级:Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304
链接:https://juejin.cn/post/6986416221323264030

收起阅读 »

今天聊:大厂如何用一道编程题考察候选人水平

进入正题 面试环节对面试官的一些挑战 面试官和候选人的知识结构可能有差异 => 可能会错过优秀的人 遇到「面霸」,频繁面试刷题,但是实际能力一般 => 招到不合适的人 要在短短半个小时到一个小时内判断一个人,其实很难 相对靠谱的做法 笔试:"...
继续阅读 »

进入正题


面试环节对面试官的一些挑战



  • 面试官和候选人的知识结构可能有差异 => 可能会错过优秀的人

  • 遇到「面霸」,频繁面试刷题,但是实际能力一般 => 招到不合适的人

  • 要在短短半个小时到一个小时内判断一个人,其实很难


相对靠谱的做法



  • 笔试:"Talk is cheap, show me the code"


笔试常见的问题



  • 考通用算法,Google 能直接搜到,失去考察意义

  • 题目难度设计有问题。要么满分,要么零分,可能错过还不错的同学

  • 和实际工作内容脱节


我认为好的笔试题



  • 上手门槛低,所有人多多少少都能写一点,不至于开天窗

  • 考点多,通过一道题可以基本摸清候选人的代码综合素养

  • 给高端的人有足够的发挥空间。同样的结果,不同的实现方式可以看出候选人的技术深度


我常用的一道笔试题


很普通的一道题


// 假设本地机器无法做加减乘除运算,需要通过远程请求让服务端来实现。
// 以加法为例,现有远程API的模拟实现

const addRemote = async (a, b) => new Promise(resolve => {
setTimeout(() => resolve(a + b), 1000)
});

// 请实现本地的add方法,调用addRemote,能最优的实现输入数字的加法。
async function add(...inputs) {
// 你的实现
}

// 请用示例验证运行结果:
add(1, 2)
.then(result => {
console.log(result); // 3
});


add(3, 5, 2)
.then(result => {
console.log(result); // 10
})

答案一
最基本的答案,如果写不出来,那大概率是通过不了了


async function add(...args) {
let res = 0;
if (args.length <= 2) return res;

for (const item of args) {
res = await addRemote(res, item);
}
return res;
}

递归版本


async function add(...args) {
let res = 0;
if (args.length === 0) return res;
if (args.length === 1) return args[0];

const a = args.pop();
const b = args.pop();
args.push(await addRemote(a, b));
return add(...args);
}

常见的问题:



  • 没有判断入参个数

  • 仍然用了本地加法


答案二
有候选人的答案如下:


// Promise链式调用版本
async function add(...args) {
return args.reduce((promiseChain, item) => {
return promiseChain.then(res => {
return addRemote(res, item);
});
}, Promise.resolve(0));

}

从这个实现可以看出:



  • 对 Array.prototype.reduce 的掌握

  • 对于 Promise 链式调用的理解

  • 考察候选人对 async function 本质的理解


这个版本至少能到 70 分


答案三
之前的答案结果都是对的,但是我们把耗时打出来,可以看到耗时和参数个数成线性关系,因为所有计算都是串行的,显然不是最优的解



更好一点的答案:


function add(...args) {
if (args.length <= 1) return Promise.resolve(args[0])
const promiseList = []
for (let i = 0; i * 2 < args.length - 1; i++) {
const promise = addRemote(args[i * 2], args[i * 2 + 1])
promiseList.push(promise)
}

if (args.length % 2) {
const promise = Promise.resolve(args[args.length - 1])
promiseList.push(promise)
}

return Promise.all(promiseList).then(results => add(...results));
}


可以看到很明显的优化。


答案四
还能再优化吗?
有些同学会想到加本地缓存


const cache = {};

function addFn(a, b) {
const key1 = `${a}${b}`;
const key2 = `${b}${a}`;
const cacheVal = cache[key1] || cache[key2];

if (cacheVal) return Promise.resolve(cacheVal);

return addRemote(a, b, res => {
cache[key1] = res;
cache[key2] = res;
return res;
});
}

加了缓存以后,我们再第二次执行相同参数加法时,可以不用请求远端,直接变成毫秒级返回



还能再优化吗?交给大家去思考


其他考察点


有些时候会让候选人将代码提交到 Github 仓库,以工作中一个实际的模块标准来开发,可以考察:



  • git 操作,commit 规范

  • 工程化素养

  • 是否有单元测试

  • 覆盖率是否达标

  • 依赖的模块版本如何设置

  • 如何配置 ci/cd

  • 文档、注释

  • ...


更加开放的一种笔试形式



  • 给一道题目,让候选人建一个 Github 仓库来完成

  • 题目有一定难度,但是可以 Google,也可以用三方模块,和我们平时做项目差不多

  • 通常面向级别较高的候选人


实际题目


// 有一个 10G 文件,每一行是一个时间戳,
// 现在要在一台 2C4G 的机器上对它进行排序,输出排序以后的文件

// 案例输入
// 1570593273487
// 1570593273486
// 1570593273488
// …

// 输出
// 1570593273486
// 1570593273487
// 1570593273488
// …



先看一个答案,看看哪里有问题


async function sort(inputFile, outputFile) {
const input = fs.createReadStream(inputFile);
const rl = readline.createInterface({ input });
const arr = [];
for await (const line of rl) {
const item = Number(line);
arr.push(item);
}
arr.sort((a, b) => a - b);

fs.writeFileSync(outputFile, arr.join('\n'));
}

10GB 的文件无法一次性放进内存里处理,内存只有 4GB


再看一个神奇的答案,只有一行代码,而且从结果来说是正确的。但不是我们笔试想要的答案。


const cp = require('child_process');

function sort(inputFile, outputFile) {
cp.exec(`sort -n ${inputFile} > ${outputFile}`);
}

解题思路



  • 既然没办法一次性在内存中排序,那我们能否将 10GB 的文件拆分成若干个小文件

  • 小文件先分别排序,然后再合并成一个大的文件


再将问题拆解



  • 拆分大文件到小文件

  • 小文件在内存里排序

  • 合并所有小文件成一个整体排序过的大文件


本题最难的点在于如何合并所有小文件。代码如何实现?



  • 这里需要用到一种数据结构:堆

  • 堆:就是用数组实现的一个二叉树

  • 堆分为:最大堆和最小堆,下面是一个最小堆(父节点小于它的子节点)


image.png


堆有一些特性:



  • 对于一个父节点来说

    • 左节点位置:父节点位置 * 2 + 1

    • 右节点位置:父节点位置 * 2 + 2



  • 很容易查找最大值 / 最小值


我们尝试把下面数组构造成一个最小堆


image.png



  • 从最后一个非叶子节点开始往前处理

  • 10 比 5 大,所以交换它们的位置


image.png



  • 然后是节点 2,符合要求不需要处理

  • 最后到顶点 3,它比左子节点大,所以要交换


image.png


完整的实现参考:github.com/gxcsoccer/e…
image.png







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

收起阅读 »

【环信IM集成指南】Android 端常见问题整理

1、如何修改系统通知中的头像和用户名系统通知是在主module中自己写的,demo中是AgreeMsgDelegate,InviteMsgDelegate,OtherMsgDelegate中去修改头像和用户名。2. 如何修改会话列表中系统消息的头像和消息里的环...
继续阅读 »

1、如何修改系统通知中的头像和用户名

系统通知是在主module中自己写的,demo中是AgreeMsgDelegate,InviteMsgDelegate,OtherMsgDelegate中去修改头像和用户名。



2. 如何修改会话列表中系统消息的头像和消息里的环信ID?

在创建系统消息的时候,去修改设设置的username。
系统消息的创建一般在好友监听和群监听中。



3. app端使用token登录,怎么获取单个用户的token?

获取单个用户的token需要调rest接口去获取。
文档中的Request Body要修改一下使用:

{"grant_type": "password",

"username": "omg2",

"password": "123456"}

参考文档:http://docs-im.easemob.com/im/server/ready/user#获取管理员权限


4. 设置群组全员禁言后,怎么获取该群组是否是全员禁言状态?
调用获取群组详情的api拿到EMGroup对象,然后再调用isAllMemberMuted去获取是否全员禁言。

EMGroup group = EMClient.getInstance().groupManager().getGroupFromServer(groupId);

group.isAllMemberMuted();


5. 登录成功后获取不到群组信息,后台查看用户是在群内,怎么解决?
检查下是否是从本地获取的群组信息,当用户打开app时,需要先从服务器拉取群组信息(放子线程中)。




6. 何时调用本地群组信息,何时调用从服务器获取群组信息?
打开App,从服务器拉取(从服务器拉取之后,sdk会保存在本地,再调用本地获取能拿到信息),当群成员更新的时候,从服务器获取。


7. 如何设置群组永久禁言?
将禁言时间设置为-1即可。
EMClient.getInstance().groupManager().muteGroupMembers(groupId, muteMembers, -1);


8. 获取群成员数总是来回变化?

检查看下是否使用的是获取完整的群成员列表。
参考文档:https://docs-im.easemob.com/im/android/basics/group#获取完整的群成员列表


9. 消息能发送成功,但是接收不到别人发给我的消息。
检查下注册的环信id是否含有大写字母,如果含有大写字母的话,需要在登录的时候去转换成小写,发送消息等操作都是需要使用小写字母的。


10. 创建聊天室,显示you hava no permission to do this
创建聊天室,只能使用rest接口去创建。
参考文档:https://docs-im.easemob.com/im/server/basics/chatroom#创建聊天室

11. 设置的自动同意添加好友,为什么添加之后好友列表里查找不到?

回答:用户A添加 用户 B为好友,如果用户B是离线状态的话,用户A的好友列表是里不显示用户B的,用户 B上线之后,用户A的好友列表中会出现用户B。


12. 怎么去设置头像和昵称?
EaseIMkit设置头像和昵称:
1.头像和昵称是Easeimkit处理的,我们只需要在application中调用easeimkit的setUserProvider根据环信id,将本地存储的头像和昵称封装到EaseUser中返回给Easeimkit即可(具体可参考demo)
2.getUserInfo中的处理逻辑是:根据username先去本地获取,如果本地获取不到,再从服务器获取,并保存到本地或者做一个三级缓存,然后,再刷新会话列表。(注:getuserinfo是同步的)


13. 如何设置是听筒模式还是扬声器?
回答:easeimkit中isSpeakerOpened()回调中去设置。



14. 怎么发送红包消息?
1.发送自定义消息并携带扩展字段(扩展字段用来判断是显示已领取还是未领取),设置红包的自定义布局(已领取和未领取),默认未领取。
2.当用户点击领取红包之后(修改扩展字段的为已领取状态),将未领取隐藏显示已领取。并发送cmd消息,在接收到cmd消息后 ,修改扩展字段的为已领取状态,并刷新ui。
3.当用户杀掉app进来之后,根据扩展字段来决定显示已领取还是未领取。


15. 发送文件大小超过20M怎么办?
发送视频文件(在EMOption中设置不上传不下载),先去上传到你们的服务器上保存,拿到url放在消息里发送给对方(setRemoteUrl),对方收到之后解析消息里的url去下载视频文件。(注:这种方法会导致所有的附件都不下载不上传)



16. demo中使用的是百度地图,我想使用高德地图,怎么使用?
需要重写聊天页面地图的点击事件,跳转到高德就可以。



17. 在聊天页面点击视频,没有反应?
需要去自定义一个fragment去继承EaseChatFragment,重写EaseChatFragment中的selectVideoFromLocal。(具体实现可以参考demo中的ChatFragment)



18. 拉取漫游数据后,展示的时间是乱的,没有按时间排序?
1. 先打印下漫游数据和本地缓存中的数据是否一致?
2.检查下在EMOption中是否设置
option.setSortMessageByServerTime(false)



19.EMMessage怎么区分离线消息还是在线消息?
EMMessage中没有方法可以去判断是离线消息还是在线消息。
实现方法:
可以在接收消息的监听中,获取到消息的时间戳与本地时间戳做对比,超过一定的时间就算离线消息。 但是前提需要保证本地的时间戳是对的。



20.集成了环信SDK后的安卓App,只要授予了定位权限,一启动就会访问用户的位置信息,如果启动App不访问用户位置信息?
在EMOptions中设置setEnableStatistics为false。



21. gradle的形式引入easeimkit,怎么去监听发送消息成功或者失败?

在easeimkit中没有将发送成功的事件,回调到fragment中,如果用户需要在自定义view中用的话,继承EaseChatRow并重写onMessageSuccess()。



22. 如何实现一键已读功能?
可以调用将所有消息置为已读的api:

EMClient.getInstance().chatManager().markAllConversationsAsRead();
参考文档链接:http://docs-im.easemob.com/im/android/basics/message#未读消息数清零



23. 怎么收不到oppo推送?
1.检查下是否已经安装官网的文档去集成了oppo推送,在控制台搜索
EMPushHelper,是否有[EMPushHelper] uploadTokenInternal success输出,如果没有输出 ,请检查下oppo的集成。
2.在完成第一步的情况下,还是收不到离线推送,请检查下密钥上传的是否正确,console后台上传密钥是:master secret ,在 app中上传的密钥的是app secret。如果不正确请删除重新上传并提交工单提供appkey+证书id,让环信技术支持在后台解禁。


24.如何设置在线消息免打扰?
单个会话的免打扰模式您可以自己去实现,要自己去维护一个免打扰list集合,当监听到有消息时,去判断下是否是免打扰用户,如果是免打扰用户,就不去提醒。
群组免打扰:可以使用rest去设置。("notification_ignore_群组id": true)


25.搜索会话列表如何根据昵称搜索(或者根据其他某个字段去搜索)。
可以把会话显示的昵称(或某个字段)放在会话的扩展里,搜索的时候遍历会话扩展里的昵称。





26.群公告的长度有限制吗?

群公告不能超过512 字符。


27.如何设置群扩展字段

1.通过EMGroupOptions的extField设置的扩展字段。
2.从服务器获取群组信息,获取getExtension
EMGroup group = EMClient.getInstance().groupManager().getGroupFromServer(groupId);group.getExtension();


28.oppo推送报空指针
Process: com.example.is, PID: 24696
java.lang.NullPointerException: Attempt to invoke virtual method 'android.content.pm.PackageManager android.content.Context.getPackageManager()' on a null object reference
at com.heytap.mcssdk.d.a(Unknown Source:7)
at com.heytap.mcssdk.d.l(Unknown Source:6)
at com.heytap.mcssdk.d.n(Unknown Source:0)
at com.heytap.msp.push.HeytapPushManager.isSupportPush(Unknown Source:4)
at com.hyphenate.push.platform.oppo.a.b(Unknown Source:0)
at com.hyphenate.push.platform.a.a(Unknown Source:6)
at com.hyphenate.push.EMPushHelper.a(Unknown Source:145)
at com.hyphenate.push.EMPushHelper.register(Unknown Source:35)
at com.hyphenate.chat.EMClient$7.run(Unknown Source:204)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:919)

需要在application中对oppo进行初始化(如下图)。







收起阅读 »

ios--离屏渲染详解

目录:1.图像显示原理2.图像显示原理2.1 图像到屏幕的流程2.2 显示器显示的流程3.卡顿、掉帧3.1 垂直同步 Vsync + 双缓冲机制 Double Buffering2.3 掉帧和屏幕卡顿的本质4.离屏渲染4.1 什么事离屏渲染、离屏渲染的过程4....
继续阅读 »

目录:

  • 1.图像显示原理
  • 2.图像显示原理
    • 2.1 图像到屏幕的流程
    • 2.2 显示器显示的流程
  • 3.卡顿、掉帧
    • 3.1 垂直同步 Vsync + 双缓冲机制 Double Buffering
    • 2.3 掉帧和屏幕卡顿的本质
  • 4.离屏渲染
    • 4.1 什么事离屏渲染、离屏渲染的过程
    • 4.2 既然离屏渲染影响界面,为什么还要用
  • 5.触发离屏渲染
  • 6.如何优化
1.引言

先来聊聊为什么要了解离屏渲染?
看看现在app开发的大环境,14年的时候在深圳,基本上每个公司都要做一个app。不做一个app你都不一定能拉倒更多的投资。再看看现在,死了一大半,现在的用户也不想去下载太多的app。一般手机上只留一些常用的,基本全是大厂的app。然后ios这行问的也就越来越难。性能优化这个绝对会问,在网上也有许多性能优化的总结,但是你不能不知道为什么这么做能优化,要知道其为什么。那么,这时候你就需要知道界面是怎么渲染的,什么时候会掉帧,什么时候会卡顿,这些都使得我们非常有必要去了解离屏渲染。
离屏渲染过程

2.图像显示原理
2.1 图像到屏幕的流程

先来看一张图,我们结合这张图来说


首先要明白的一个东西是Render Server 进程,app本身其实并不负责渲染,渲染是有独立的进程负责的,它就是Render Server 。

当我们在代码里设置修改了UI界面的时候,其实它本质是通过Core Animation修改CALayer。在后续的核心动画总结中 我们会说到UIView和CALayer的关系,以及核心动画的设置等等,这个知识点有点多,需要单独详细的总结出来。所以最后按照图片中的流程显示。

  • 首先,有app处理事件(Handle Events),例如:用户点击了一个按钮,它会触发其他的视图的一个动画等
  • 其次,app通过CPU完成对显示内容的计算 例如:视图的创建,视图的布局 图片文本的绘制等。在完成了对显示内容的计算之后,app对图层进行打包,并在下一次Runloop时,将其发送至Render Server
  • 上面我们提到,Render Server负责渲染。Render Server通过执行Open GL、Core Graphics Metal相关程序。 调用GPU
  • GPU在物理层完成了对图像的渲染。

说到这我们就要停一下,我们来看下一个图




上面的流程图 细化了GPU到控制器的这一个过程。
GPU 拿到位图后执行顶点着色、图元装配、光栅化、片段着色等,最后将渲染的结果交到了Frame Buffer(帧缓存区当中)
然后视频控制器从帧缓存区中拿到要显示的对象,显示到屏幕上
图片中的黄色虚线暂时不用管,下面在说垂直同步信号的时候,就明白了。
这是从我们代码中设置UI,然后到屏幕的一个过程。

2.2 显示器显示的过程

现在从帧缓存中拿到了渲染的视图,又该怎么显示到显示器上面呢?

先来看一张图



从图中我们也能大致的明白显示的一个过程。

显示器的电子束从屏幕的左上方开始逐行显示,当第一行扫描完之后接着第二行 又是从左到右,就这样一直到屏幕的最下面扫描完成。我们都知道。手机它是有屏幕的刷新次数的。安卓的现在好多是120的,ios是60。1秒刷新60次,当我们扫描完成以后,屏幕刷新,然后视图就会显示出来。

3.UI卡顿 掉帧
3.1垂直同步 Vsync + 双缓冲机制 Double Buffering

首先我们了解了上面渲染的过程以后,需要考虑遇到一些特别的情况下,该怎么办?在我们代码里写了一个很复杂的UI视图,然后CPU计算布局、GPU渲染,最后放到缓存区。如果在电子束开始扫描新的一帧时,位图还没有渲染好,而是在扫描到屏幕中间时才渲染完成,被放入帧缓冲器中 。
那么已扫描的部分就是上一帧的画面,而未扫描的部分就是新一帧的图像,这样是不是就造成了屏幕撕裂了。

但是,在我们平常开发的过程遇到过屏幕撕裂的问题吗?没有吧,这是为什么呢?
显然是苹果做了优化操作了。也就是垂直同步 Vsync + 双缓冲机制 Double Buffering。

垂直同步 Vsync
垂直同步 Vsync相当于给帧缓存加了锁,还记得上面说到的那个黄色虚线嘛,在我们扫描完一帧以后,就会发出一个垂直同步的信号,通知开始扫描下一帧的图像了。他就像一个位置秩序的,你得给我排队一个一个来,别插队。插队的后果就是屏幕撕裂。
双缓冲机制 Double Buffering
扫描显示排队进行了,这样在进行下一帧的位图传入的时候,也就意味着我要立刻拿到位图。不能等CPU+GPU计算渲染后再给位图,这样就影响性能。要怎么解决这个问题呢?肯定是 在你快要渲染之前你就要把这些都完成了。你就像排队打针一样,为了节省时间肯定事先都会挽起袖子,到医生那时,直接一针下去了事。扯远了 哈哈。想预先渲染好,就需要另外一个缓存来放下一帧的位图,在它需要扫描的时候,再把渲染好的位图给了帧缓存,帧缓存拿到以后 开始快乐的扫描 显示。
一个图解释




3.2 掉帧卡顿

垂直同步和双缓存机制完美的解决了屏幕撕裂的问题,但是又引出一个新的问题:掉帧。
掉帧是什么意思呢?从网上copy了一份图



其实很好理解,上面我们说了ios的屏幕刷新是60次,那么在一次刷新的过程中,我们CPU+GPU它没有把新渲染的位图放到帧缓存区,这时候是不是还是显示的原来的图像。当下刷新下一帧的时候,拿到了新的位图,这里是不是就丢失了一帧。

卡顿的根本原因:
CPU和GPU渲染流水线耗时过长 掉帧
我们平常写界面的时候,通过一些开源的库或者自己使用runloop写的库来检测界面卡顿的时候,屏幕刷新率在50以上就很可以了。一般人哪能体验到掉了10帧。你要刷新率是30,那卡顿想过就很明显了。

4 离屏渲染
4.1什么是离屏渲染 离屏渲染的过程

是指在GPU在当前屏幕缓冲区以外开辟一个缓冲区进行渲染操作.
过程:首先会创建一个当前屏幕缓冲区以外的新缓存区,屏幕渲染会有一个上下文环境,离屏渲染的过程就是切花上下文环境,现充当前屏幕切换到离屏,等结束以后又将上下文切换回来。所以需要更长的时间来处理。时间一长就可能造成掉帧。
并且 Offscreen Buffer离屏缓存 本身就需要额外的空间,大量的离屏渲染可能造成内存过大的压力。而且离屏缓存区并不是没有限制大小的,它是不能超过屏幕总像素的2.5倍。

4.2为什么要使用离屏渲染

1.一些特殊效果需要使用额外的 Offscreen Buffer 来保存渲染的中间状态,所以不得不使用离屏渲染。
2.处于效率目的,可以将内容提前渲染保存在 Offscreen Buffer 中,达到复用的目的。
当使用圆角,阴影,遮罩的时候,图层属性的混合体被指定为在未预合成之前(下一个VSync信号开始前)不能直接在屏幕中绘制,所以就需要屏幕外渲染。

5.触发离屏渲染
  1. 为图层设置遮罩(layer.mask)
  2. 图层的layer. masksToBounds/view.clipsToBounds属性设置为true
  3. 将图层layer. allowsGroupOpacity设置为yes和layer. opacity<1.0
  4. 为图层设置阴影(layer.shadow)
  5. 为图层设置shouldRasterize光栅化
    6 复杂形状设置圆角等
    7 渐变
    8 文本(任何种类,包括UILabel,CATextLayer,Core Text等)
    9 使用CGContext在drawRect :方法中绘制大部分情况下会导致离屏渲染,甚至仅仅是一个空的实现。
6 离屏渲染的优化
1 圆角优化

方法一

iv.layer.cornerRadius = 30;
iv.layer.masksToBounds = YES;
方法二
利用mask设置圆角,利用贝塞斯曲线和CAShapeLayer来完成

CAShapeLayer *mask1 = [[CAShapeLayer alloc] init];
mask1.opacity = 0.5;
mask1.path = [UIBezierPath bezierPathWithOvalInRect:iv.bounds].CGPath;
iv.layer.mask = mask1;
方法三
利用CoreGraphics画一个圆形上下文,然后把图片绘制上去

- (void)setCircleImage
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UIImage * circleImage = [image imageWithCircle];
dispatch_async(dispatch_get_main_queue(), ^{
imageView.image = circleImage;
});
});
}


#import "UIImage+Addtions.h"
@implementation UIImage (Addtions)
//返回一张圆形图片
- (instancetype)imageWithCircle
{
UIGraphicsBeginImageContextWithOptions(self.size, NO, 0);
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, self.size.width, self.size.height)];
[path addClip];
[self drawAtPoint:CGPointZero];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
}
shadows(阴影)

设置阴影后,设置CALayer的shadowPath

view.layer.shadowPath = [UIBezierPath pathWithCGRect:view.bounds].CGPath;

mask(遮罩)

不使用mask
使用混合图层 使用混合图层,在layer上方叠加相应mask形状的半透明layer

sublayer.contents = (id)[UIImage imageNamed:@"xxx"].CGImage;
[view.layer addSublayer:sublayer];
allowsGroupOpacity(组不透明)

关闭 allowsGroupOpacity 属性,按产品需求自己控制layer透明度

edge antialiasing(抗锯齿)

不设置 allowsEdgeAntialiasing 属性为YES(默认为NO)

当视图内容是静态不变时,设置 shouldRasterize(光栅化)为YES,此方案最为实用方便


view.layer.shouldRasterize = true;
view.layer.rasterizationScale = view.layer.contentsScale;

如果视图内容是动态变化的,例如cell中的图片,这个时候使用光栅化会增加系统负荷。

作者:Harry__Li
链接:https://www.jianshu.com/p/3c3383bdeb71


收起阅读 »

iOS-分页控制器

使用:1、创建方法1.1 导入头文件#import "XLPageViewController.h"1.2 遵守协议@interface ViewController ()<XLPageViewControllerDelegate, XLPageView...
继续阅读 »




使用:

1、创建方法

1.1 导入头文件

#import "XLPageViewController.h"
1.2 遵守协议
@interface ViewController ()<XLPageViewControllerDelegate, XLPageViewControllerDataSrouce>
1.3 创建外观配置类

注:config负责所有的外观配置,defaultConfig方法设定了默认参数,使用时可按需配置。 →Config属性列表

  XLPageViewControllerConfig *config = [XLPageViewControllerConfig defaultConfig];

1.4 创建分页控制器

注:需要把pageViewController添加为当前视图控制器的子视图控制器,才能实现子视图控制器中的界面跳转。

  XLPageViewController *pageViewController = [[XLPageViewController alloc] initWithConfig:config];
pageViewController.view.frame = self.view.bounds;
pageViewController.delegate = self;
pageViewController.dataSource = self;
[self.view addSubview:pageViewController.view];
[self addChildViewController:pageViewController];
2、协议

2.1 XLPageViewControllerDelegate

//回调切换位置
- (void)pageViewController:(XLPageViewController *)pageViewController didSelectedAtIndex:(NSInteger)index;

2.2 XLPageViewControllerDataSrouce

@required

//根据index创建对应的视图控制器,每个试图控制器只会被创建一次。
- (UIViewController *)pageViewController:(XLPageViewController *)pageViewController viewControllerForIndex:(NSInteger)index;
//根据index返回对应的标题
- (NSString *)pageViewController:(XLPageViewController *)pageViewController titleForIndex:(NSInteger)index;
//返回分页数
- (NSInteger)pageViewControllerNumberOfPage;

@optional

//标题cell复用方法,自定义标题cell时用到
- (__kindof XLPageTitleCell *)pageViewController:(XLPageViewController *)pageViewController titleViewCellForItemAtIndex:(NSInteger)index;

3、自定义标题cell

3.1 创建一个XLPageTitleCell的子类

#import "XLPageTitleCell.h"

@interface CustomPageTitleCell : XLPageTitleCell

@end

3.2 注册cell、添加创建cell

//需要先注册cell
[self.pageViewController registerClass:CustomPageTitleCell.class forTitleViewCellWithReuseIdentifier:@"CustomPageTitleCell"];
//自定义标题cell创建方法
- (XLPageTitleCell *)pageViewController:(XLPageViewController *)pageViewController titleViewCellForItemAtIndex:(NSInteger)index {
CustomPageTitleCell *cell = [pageViewController dequeueReusableTitleViewCellWithIdentifier:@"CustomPageTitleCell" forIndex:index];
return cell;
}

3.3 复写cell父类方法

//通过此父类方法配置标题cell是否被选中样式
- (void)configCellOfSelected:(BOOL)selected {

}

//通过此父类方法配置标题cell动画;type:区分是当前选中cell/将要被选中的cell;progress:动画进度0~1
- (void)showAnimationOfProgress:(CGFloat)progress type:(XLPageTitleCellAnimationType)type {

}

4、特殊情况处理

4.1 和子view手势冲突问题

pageViewController的子视图中存在可滚动的子view,例如UISlider、UIScrollView等,如果子view和pageViewController发生滚动冲突时,可设置子view的xl_letMeScrollFirst属性为true。

  UISlider *slider = [[UISlider alloc] init];
slider.xl_letMeScrollFirst = true;
[childVC.view addSubview:slider];

4.2 全屏返回手势问题

pageViewController和全屏返回手势一起使用时,需要将其它手势的delegate的类名添加到respondOtherGestureDelegateClassList属性中。当滚动到第一个分页时,向右滑动会优先响应全屏返回。以FDFullscreenPopGesture为例:

self.pageViewController.respondOtherGestureDelegateClassList = @[@"_FDFullscreenPopGestureRecognizerDelegate"];

5、注意事项

使用时需注意标题不要重复标题是定位ViewController的唯一ID。


源码下载:XLPageViewController-master.zip 

常见问题及demo:https://github.com/mengxianliang/XLPageViewController





收起阅读 »

iOS - 呼吸动画库

先看效果

先看效果



需求和实现思路

具体要求

  • 内部头像呼吸放大缩小 无限循环
  • 每次放大同时需要背景还有一张图也放大 并且透明
  • 点击缩放整个背景视图


实现思路

首先 需要使用创建一个Layer 装第一个无限放大缩小的呼吸的图 背景也需要一个Layer 做 放大+透明度渐变的动画组并且也放置一张需要放大渐变的图片

最后点击触发. 添加一个一次性的缩放动画即可

呼吸动画layer和动画

呼吸layer

CALayer *layer = [CALayer layer];
layer.position = CGPointMake(kHeartSizeWidth/2.0f, kHeartSizeHeight/2.0f);
layer.bounds = CGRectMake(0, 0, kHeartSizeWidth/2.0f, kHeartSizeHeight/2.0f);
layer.backgroundColor = [UIColor clearColor].CGColor;
layer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"breathImage"].CGImage);
layer.contentsGravity = kCAGravityResizeAspect;
[self.heartView.layer addSublayer:layer];
复制代码

kHeartSizeHeight 和kHeartSizeWidth 是常量 demo中写好了100

加帧动画

CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"];
animation.values = @[@1.f, @1.4f, @1.f];
animation.keyTimes = @[@0.f, @0.5f, @1.f];
animation.duration = 1; //1000ms
animation.repeatCount = FLT_MAX;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[animation setValue:kBreathAnimationKey forKey:kBreathAnimationName];
[layer addAnimation:animation forKey:kBreathAnimationKey];
复制代码

差值器也可以自定义 例如:

[CAMediaTimingFunction functionWithControlPoints:0.33 :0 :0.67 :1]
复制代码

这里我做的持续时常1秒

放大渐变动画group

创建新layer

CALayer *breathLayer = [CALayer layer];
breathLayer.position = layer.position;
breathLayer.bounds = layer.bounds;
breathLayer.backgroundColor = [UIColor clearColor].CGColor;
breathLayer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"breathImage"].CGImage);
breathLayer.contentsGravity = kCAGravityResizeAspect;
[self.heartView.layer insertSublayer:breathLayer below:layer];
//[self.heartView.layer addSublayer:breathLayer];
复制代码

这里用的是放在 呼吸layer后边 如果想放在呼吸layer前边 就把里面注释打开 然后注掉 inert那行代码

动画组 包含 放大 渐变


//缩放
CAKeyframeAnimation *scaleAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"];
scaleAnimation.values = @[@1.f, @2.4f];
scaleAnimation.keyTimes = @[@0.f,@1.f];
scaleAnimation.duration = animation.duration;
scaleAnimation.repeatCount = FLT_MAX;
scaleAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
//透明度
CAKeyframeAnimation *opacityAnimation = [CAKeyframeAnimation animation];
opacityAnimation.keyPath = @"opacity";
opacityAnimation.values = @[@1.f, @0.f];
opacityAnimation.duration = 0.4f;
opacityAnimation.keyTimes = @[@0.f, @1.f];
opacityAnimation.repeatCount = FLT_MAX;
opacityAnimation.duration = animation.duration;
opacityAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];

//动画组
CAAnimationGroup *scaleOpacityGroup = [CAAnimationGroup animation];
scaleOpacityGroup.animations = @[scaleAnimation, opacityAnimation];
scaleOpacityGroup.removedOnCompletion = NO;
scaleOpacityGroup.fillMode = kCAFillModeForwards;
scaleOpacityGroup.duration = animation.duration;
scaleOpacityGroup.repeatCount = FLT_MAX;
[breathLayer addAnimation:scaleOpacityGroup forKey:kBreathScaleName];
复制代码

点击缩放动画

跟第一个一样 只不过 执行次数默认一次 执行完就可以了

- (void)shakeAnimation {
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"];
animation.values = @[@1.0f, @0.8f, @1.f];
animation.keyTimes = @[@0.f,@0.5f, @1.f];
animation.duration = 0.35f;
animation.timingFunctions = @[[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
[self.heartView.layer addAnimation:animation forKey:@""];
}
复制代码

手势触发的时候 调用一下 

源码及demo地址:https://github.com/sunyazhou13/BreathAnimation



12个出现频率最高的iOS技术面试题及答案

这篇文章给大家总结了在iOS面试的时候可能会遇到的12个技术面试题,以及这些面试题但答案,这些答案只是给大家一些参考,大家可以再结合自己理解进行回答,有需要的朋友们下面来一起看看吧。前言随着移动互联网科技不断的发展和创新,如今无论是公司还是开发者或设计师个人而...
继续阅读 »

这篇文章给大家总结了在iOS面试的时候可能会遇到的12个技术面试题,以及这些面试题但答案,这些答案只是给大家一些参考,大家可以再结合自己理解进行回答,有需要的朋友们下面来一起看看吧。

前言

随着移动互联网科技不断的发展和创新,如今无论是公司还是开发者或设计师个人而言,面试都是一项耗时耗钱的项目,而面对iOS开发者及设计师在面试时可能会遇到的问题进行了筛选与汇总。下面我们一起来一下看看吧。

一、如何绘制UIView?

绘制一个UIView最灵活的方法就是由它自己完成绘制。实际上你不是绘制一个UIView,而是子类化一个UIView并赋予绘制自己的能力。当一个UIView需要执行绘制操作时,drawRect:方法就会被调用,覆盖此方法让你获得绘图操作的机会。当drawRect:方法被调用,当前图形的上下文也被设置为属于视图的图形上下文,你可以使用Core Graphic或者UIKit提供的方法将图形画在该上下文中。

二、什么是MVVM?主要目的是什么?优点有哪些?

MVVM即 Model-View-ViewModel

1.View主要用于界面呈现,与用户输入设备进行交互、

2.ViewModel是MVVM架构中最重要的部分,ViewModel中包含属性,方法,事件,属性验证等逻辑,负责ViewModel之间的通讯

3.Model就是我们常说的数据模型,用于数据的构造,数据的驱动,主要提供基础实体的属性。

MVVM主要目的是分离视图和模型

MVVM优点:低耦合,可重用性,独立开发,可测试

三、get请求与post请求的区别

1.get是向服务器发索取数据的一种请求,而post是向服务器提交数据的一种请求

2.get没有请求体,post有请求体

3.get请求的数据会暴露在地址栏中,而post请求不会,所以post请求的安全性比get请求号

4.get请求对url长度有限制,而post请求对url长度理论上是不会收限制的,但是实际上各个服务器会规定对post提交数据大小进行限制。

四、谈谈你对多线程开发的理解?ios中有几种实现多线程的方法?

好处:

1.使用多线程可以把程序中占据时间长的任务放到后台去处理,如图片,视频的下载;

2.发挥多核处理器的优势,并发执行让系统运行的更快,更流畅,用户体验更好;

缺点:

1.大量的线程降低代码的可读性;

2.更多的线程需要更多的内存空间;

3当多个线程对同一个资源出现争夺的时候要注意线程安全的问题。

ios有3种多线程编程的技术:1.NSThread,2.NSOperationQueue,3.gcd;

五、XMPP工作原理;xmpp系统特点

原理:

1.所有从一个client到另一个clientjabber消息和数据都要通过xmpp server

2.client链接到server

3.server利用本地目录系统的证书对其认证

4.server查找,连接并进行相互认证

5.client间进行交互

特点:1)客户机/服务器通信模式;2)分布式网络;3)简单的客户端;4)XML的数据格式

六、地图的定位是怎么实现的?

1.导入了CoreLocation.framework

2.ios8以后,如果需要使用定位功能,就需要请求用户授权,在首次运行时会弹框提示

3.通过本机自带的gps获取位置信息(即经纬度)

七、苹果内购实现流程

程序通过bundle存储的plist文件得到产品标识符的列表。

程序向App Store发送请求,得到产品的信息。

App Store返回产品信息。

程序把返回的产品信息显示给用户(App的store界面)

用户选择某个产品

程序向App Store发送支付请求

App Store处理支付请求并返回交易完成信息。

App获取信息并提供内容给用户。

八、支付宝,微信等相关类型的sdk的集成

1.在支付宝开发平台创建应用并获取APPID

2.配置密钥

3.集成并配置SDK

4.调用接口(如交易查询接口,交易退款接口)

九、 gcd产生死锁的原因及解锁的方法

产生死锁的必要条件:1.互斥条件,2.请求与保持条件,3.不剥夺条件,4.循环等待条件。

解决办法:采用异步执行block。

十、生成二维码的步骤

1.使用CIFilter滤镜类生成二维码

2.对生成的二维码进行加工,使其更清晰

3.自定义二维码背景色、填充色

4.自定义定位角标

5.在二维码中心插入小图片


十一、在使用XMPP的时候有没有什么困难

发送附件(图片,语音,文档...)时比较麻烦

XMPP框架没有提供发送附件的功能,需要自己实现

实现方法,把文件上传到文件服务器,上传成功后获取文件保存路径,再把附件的路径发送给好友



十二、是否使用过环信,简单的说下环信的实现原理

环信是一个即时通讯的服务提供商

环信使用的是XMPP协议,它是再XMPP的基础上进行二次开发,对服务器Openfire和客户端进行功能模型的添加和客户端SDK的封装,环信的本质还是使用XMPP,基于Socket的网络通信

环信内部实现了数据缓存,会把聊天记录添加到数据库,把附件(如音频文件,图片文件)下载到本地,使程序员更多时间是花到用户体验体验上。



作者:iOS鑫
链接:https://www.jianshu.com/p/d95967869aed
收起阅读 »

最新iOS开发常见面试题-基础篇

1.iOS线程与进程的区别和联系?进程和线程都是由操作系统所体会的程序运行的基本单元,系统利用该基本单元实现系统对应用的并发性。程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而...
继续阅读 »

1.iOS线程与进程的区别和联系?

进程和线程都是由操作系统所体会的程序运行的基本单元,系统利用该基本单元实现系统对应用的并发性。

程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

2.iOS 如何找到最合适的控件来处理事件?

自己是否能接收触摸事件?
触摸点是否在自己身上?
从后往前遍历子控件,重复前面的两个步骤
如果没有符合条件的子控件,那么就自己最适合处理

3.iOS static 关键字的作用?

(1)函数体内 static 变量的作用范围为该函数体,不同于 auto 变量,该变量的内存只被分配一次,

因此其值在下次调用时仍维持上次的值;

(2)在模块内的 static 全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问;

(3)在模块内的 static 函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明

它的模块内;

(4)在类中的 static 成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;

(5)在类中的 static 成员函数属于整个类所拥有,这个函数不接收 this 指针,因而只能访问类的static 成员变量。

4.iOS UIEvent对象的作用与常见属性?

每产生一个事件,就会产生一个UIEvent对象

UIEvent : 称为事件对象,记录事件产生的时刻和类型
常见属性 

  //事件类型
//@property(nonatomic,readonly) UIEventType type;
//@property(nonatomic,readonly) UIEventSubtype subtype;
//事件产生的时间
@property(nonatomic,readonly) NSTimeInterval timestamp;
UIEvent还提供了相应的方法可以获得在某个view上面的触摸对象(UITouch)

4.ViewController 的 loadView, viewDidLoad, viewDidUnload 分别是在什么时候调用的?在自定义ViewController的时候这几个函数里面应该做什么工作?

viewDidLoad在view 从nib文件初始化时调用,loadView在controller的view为nil时调用。此方法在编程实现view时调用,view 控制器默认会注册memory warning notification,当view controller的任何view 没有用的时候,viewDidUnload会被调用,在这里实现将retain 的view release,如果是retain的IBOutlet view 属性则不要在这里release,IBOutlet会负责release 。

5.object-c 的优缺点 ?

objc优点:

  1. Cateogies

  2. Posing

  3. 动态识别

  4. 指标计算

5)弹性讯息传递

  1. 不是一个过度复杂的 C 衍生语言

  2. Objective-C 与 C++ 可混合编程

缺点:

  1. 不支援命名空间

  2. 不支持运算符重载

3)不支持多重继承

4)使用动态运行时类型,所有的方法都是函数调用,所以很多编译时优化方法都用不到。(如内联函数等),性能低劣。

6.iOS引用与指针有什么区别?

1.引用必须被初始化,指针不必。
2.引用初始化以后不能被改变,指针可以改变所指的对象。
3.不存在指向空值的引用,但是存在指向空值的指针。

7.iOS堆和栈的区别 ?

管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。

申请大小:

栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。

堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出

分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。

分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的。

8.什么时候用delegate,什么时候用Notification?

delegate针对one-to-one关系,并且reciever可以返回值 给sender,notification 可以针对one-to-one/many/none,reciever无法返回值给sender.所以,delegate用于sender希望接受到 reciever的某个功能反馈值,notification用于通知多个object某个事件。

9.iOS UITouch对象的作用与常见属性?

当用户用一根手指触摸屏幕时,会创建一个与手指相关联的UITouch对象 一根手指对应一个UITouch对象

UITouch的作用:

保存着跟手指相关的信息,比如触摸的位置、时间、阶段
当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置
当手指离开屏幕时,系统会销毁相应的UITouch对象
UITouch的常见属性

//触摸产生时所处的窗口
@property(nonatomic,readonly,retain) UIWindow *window;

//触摸产生时所处的视图
@property(nonatomic,readonly,retain) UIView *view;

//短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击
@property(nonatomic,readonly) NSUInteger tapCount;

//记录了触摸事件产生或变化时的时间,单位是秒
@property(nonatomic,readonly) NSTimeInterval timestamp;

//当前触摸事件所处的状态
@property(nonatomic,readonly) UITouchPhase phase;

UITouch的常见方法

   //返回值表示触摸在view上的位置
//这里返回的位置是针对view的坐标系的(以view的左上角为原点(0, 0))
//调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
- (CGPoint)locationInView:(UIView *)view;
// 该方法记录了前一个触摸点的位置
- (CGPoint)previousLocationInView:(UIView *)view;


10.object-c 的内存管理 ?

如果您通过分配和初始化(比如[[MyClass alloc] init])的方式来创建对象,您就拥有这个对象,需要负责该对象的释放。这个规则在使用NSObject的便利方法new 时也同样适用。

如果您拷贝一个对象,您也拥有拷贝得到的对象,需要负责该对象的释放。

如果您保持一个对象,您就部分拥有这个对象,需要在不再使用时释放该对象。

反过来,如果您从其它对象那里接收到一个对象,则您不拥有该对象,也不应该释放它(这个规则有少数的例外)

11.iOS单件实例是什么 ?

Foundation 和 Application Kit 框架中的一些类只允许创建单件对象,即这些类在当前进程中的唯一实例。举例来说,NSFileManager 和NSWorkspace 类在使用时都是基于进程进行单件对象的实例化。当向这些类请求实例的时候,它们会向您传递单一实例的一个引用,如果该实例还不存在,则首先进行实例的分配和初始化。单件对象充当控制中心的角色,负责指引或协调类的各种服务。如果类在概念上只有一个实例(比如NSWorkspace),就应该产生一个单件实例,而不是多个实例;如果将来某一天可能有多个实例,您可以使用单件实例机制,而不是工厂方法或函数。

12.iOS类工厂方法是什么 ?

类工厂方法的实现是为了向客户提供方便,它们将分配和初始化合在一个步骤中,返回被创建的对象,并进行自动释放处理。这些方法的形式是+ (type)className...(其中 className不包括任何前缀)。

工厂方法可能不仅仅为了方便使用。它们不但可以将分配和初始化合在一起,还可以为初始化过程提供对象的分配信息,类工厂方法的另一个目的是使类(比如NSWorkspace)提供单件实例。虽然init...方法可以确认一个类在每次程序运行过程只存在一个实例,但它需要首先分配一个“生的”实例,然后还必须释放该实例,工厂方法则可以避免为可能没有用的对象盲目分配内存。

13.一个指针可以是volatile 吗?解释为什么。

是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。

14.iOS 类别的局限性 ?

有两方面局限性:

(1)无法向类中添加新的实例变量,类别没有位置容纳实例变量。

(2)名称冲突,即当类别中的方法与原始类方法名称冲突时,类别具有更高的优先级。类别方法将完全取代初始方法从而无法再使用初始方法。

无法添加实例变量的局限可以使用字典对象解决

15.什么是iOS键-值,键路径是什么 ?

模型的性质是通过一个简单的键(通常是个字符串)来指定的。视图和控制器通过键来查找相应的属性值。在一个给定的实体中,同一个属性的所有值具有相同的数据类型。键-值编码技术用于进行这样的查找—它是一种间接访问对象属性的机制。

键路径是一个由用点作分隔符的键组成的字符串,用于指定一个连接在一起的对象性质序列。第一个键的性质是由先前的性质决定的,接下来每个键的值也是相对于其前面的性质。键路径使您可以以独立于模型

实现的方式指定相关对象的性质。通过键路径,您可以指定对象图中的一个任意深度的路径,使其指向相关对象的特定属性。

16.iOS 类别的作用 ?

类别主要有3个作用:

(1)将类的实现分散到多个不同文件或多个不同框架中。

(2)创建对私有方法的前向引用。

(3)向对象添加非正式协议。

17.sprintf,strcpy,memcpy使用上有什么要注意的地方 ?

strcpy是一个字符串拷贝的函数,它的函数原型为strcpy(char dst, ct char *src);

将 src开始的一段字符串拷贝到dst开始的内存中去,结束的标志符号为'\0',由于拷贝的长度不是由我们自己控制的,所以这个字符串拷贝很容易出错。具备字符串拷贝功能的函数有memcpy,这是一个内存拷贝函数,它的函数原型为memcpy(char dst, c*t char src, unsigned int len);

将长度为len的一段内存,从src拷贝到dst中去,这个函数的长度可控。但是会有内存叠加的问题。

sprintf是格式化函数。将一段数据通过特定的格式,格式化到一个字符串缓冲区中去。sprintf格式化的函数的长度不可控,有可能格式化后的字符串会超出缓冲区的大小,造成溢出。

14答案是:

a) int a; // An integer

b) int *a; // A pointer to an integer

c) int **a; // A pointer to a pointer to an integer

d) int a[10]; // An array of 10 integers

e) int *a[10]; // An array of 10 pointers to integers

f) int (*a)[10]; // A pointer to an array of 10 integers

g) int (*a)(int); // A pointer to a function a that takes an integer argument and returns an integer

h) int (a[10])(int); // An array of 10 pointers to functi that take an integer argument and return an integer

18.readwrite,readonly,assign,retain,copy,nonatomic属性的作用

@property是一个属性访问声明,扩号内支持以下几个属性:

1,getter=getterName,setter=setterName,设置setter与getter的方法名

2,readwrite,readonly,设置可供访问级别

2,assign,setter方法直接赋值,不进行任何retain操作,为了解决原类型与环循引用问题

3,retain,setter方法对参数进行release旧值再retain新值,所有实现都是这个顺序(CC上有相关资料)

4,copy,setter方法进行Copy操作,与retain处理流程一样,先旧值release,再Copy出新的对象,retainCount为1。这是为了减少对上下文的依赖而引入的机制。copy是在你不希望a和b共享一块内存时会使用到。a和b各自有自己的内存。

5,nonatomic,非原子性访问,不加同步,多线程并发访问会提高性能。注意,如果不加此属性,则默认是两个访问方法都为原子型事务访问。锁被加到所属对象实例级(我是这么理解的...)。

atomic和nonatomic用来决定编译器生成的getter和setter是否为原子操作。在多线程环境下,原子操作是必要的,否则有可能引起错误的结果。

19"NSMutableString *"这个数据类型则是代表"NSMutableString"对象本身,这两者是有区别的。

NSString只是对象的指针而已。

面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。

面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。


作者:iOS鑫
链接:https://www.jianshu.com/p/48a5b53c63e8

收起阅读 »

iOS面试备战-网络篇

计算机网络是计算机科学与技术专业的必修课,也是移动端,前端,后端都会涉及并用到的知识点,可想而知它的重要性。所以它也成为了iOS面试中经常被问及的问题。准备面试的话,网络相关的知识点一定不能错过。这里总结了一些我认为有用的和最近面试遇到的网络相关知识点。计算机...
继续阅读 »
计算机网络是计算机科学与技术专业的必修课,也是移动端,前端,后端都会涉及并用到的知识点,可想而知它的重要性。所以它也成为了iOS面试中经常被问及的问题。准备面试的话,网络相关的知识点一定不能错过。这里总结了一些我认为有用的和最近面试遇到的网络相关知识点。


计算机网络是如何分层的

网络有两种分层模型,一种是ISO(国际标准化组织)制定的OSI(Open System Interconnect)模型,它将网络分为七层。一种是TCP/IP的四层网络模型。OSI是一种学术上的国际标准,理想概念,TCP/IP是事实上的国际标准,被广泛应用于现实生活中。两者的关系可以看这个图:




注:也有说五层模型的,它跟四层模型的区别就是,在OSI模型中的数据链路层和物理层,前者将其作为两层,后者将其合并为一层称为网络接口层。一般作为面试题的话都是需要讲出OSI七层模型的。

各个分层的含义以及它们之间的关系用这张图表示:



Http协议

http协议特性

  • HTTP 协议构建于 TCP/IP 协议之上,是一个应用层协议,默认端口号是 80
  • 灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。
  • 无状态:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。
  • 无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传。

请求方法

  • GET:请求获取Request-URI标识的资源,请求参数附加在url上,明文展示。

  • POST:在Request-URI所标识的资源后附加新的数据,常用于修改服务器资源或者提交资源到服务器。POST请求体是放到body中的,可以指定编码方式,更加安全。

  • HEAD:请求获取由Request-URI所标识的资源的响应消息报头。

  • PUT:请求服务器存储一个资源,并用Request-URI作为其标识。

  • DELETE:请求服务器删除Request-URI所标识的资源。

  • TRACE:请求服务器回送收到的请求信息,主要用于测试或诊断。

  • OPTIONS:请求查询服务器的性能,或者查询与资源相关的选项和需求。

请求和响应报文

在Chrome查看其请求的Headers信息。

General

这里标记了请求的URL,请求方法为GET。状态码为304,代表文件未修改,可以直接使用缓存的文件。远程地址为185.199.111.153:443,此IP为Github 服务器地址,是因为我的博客是部署在GitHub上的。

除了304还有别的状态码,分别是:

  • 200 OK 客户端请求成功
  • 301 Moved Permanently 请求永久重定向
  • 302 Moved Temporarily 请求临时重定向
  • 304 Not Modified 文件未修改,可以直接使用缓存的文件。
  • 400 Bad Request 由于客户端请求有语法错误,不能被服务器所理解。
  • 401 Unauthorized 请求未经授权。这个状态代码必须和WWW-Authenticate报头域一起使用
  • 403 Forbidden 服务器收到请求,但是拒绝提供服务。服务器通常会在响应正文中给出不提供服务的原因
  • 404 Not Found 请求的资源不存在,例如,输入了错误的URL
  • 500 Internal Server Error 服务器发生不可预期的错误,导致无法完成客户端的请求。
  • 503 Service Unavailable 服务器当前不能够处理客户端的请求,在一段时间之后,服务器可能会恢复正常。

Response Headers


content-encoding:用于指定压缩算法

content-length:资源的大小,以十进制字节数表示。

content-type:指示资源的媒体类型。图中所示内容类型为html的文本类型,文字编码方式为utf-8

last-modified:上次内容修改的日期,为6月8号

status:304 文件未修改状态码

注:其中content-type在响应头中代表,需要解析的格式。在请求头中代表上传到服务器的内容格式。

Request Headers




method:GET请求

:path:url路径

:scheme:https请求

accept:通知服务器可以返回的数据类型。

accept-encoding:编码算法,通常是压缩算法,可用于发送回的资源

accept-language:通知服务器预期发送回的语言类型。这是一个提示,并不一定由用户完全控制:服务器应该始终注意不要覆盖用户的显式选择(比如从下拉列表中选择语言)。

cookie:浏览器cookie

user-agent:用户代理,标记系统和浏览器内核

TCP三次握手和四次挥手的过程以及为什么要有三次和四次

在了解TCP握手之前我们先看下TCP的报文样式:

TCP三次握手

示意图如下:



三次握手是指建立一个TCP连接时,需要客户端和服务器总共发送3个数据包。

1、第一次握手(SYN=1, seq=x)

客户端发送一个 TCP 的 SYN 标志位置1的包,指明客户端打算连接的服务器的端口,以及初始序号 X,保存在包头的序列号(Sequence Number)字段里。

发送完毕后,客户端进入 SYN_SEND 状态。

2、第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1)

服务器发回确认包(ACK)应答。即 SYN 标志位和 ACK 标志位均为1。服务器端选择自己 ISN 序列号,放到 Seq 域里,同时将确认序号(Acknowledgement Number)设置为客户的 ISN 加1,即X+1。 发送完毕后,服务器端进入 SYN_RCVD 状态。

3、第三次握手(ACK=1, ACKnum=y+1)

客户端再次发送确认包(ACK),SYN 标志位为0,ACK 标志位为1,并且把服务器发来 ACK 的序号字段+1,放在确定字段中发送给对方,并且在数据段放写ISN的+1

发送完毕后,客户端进入 ESTABLISHED 状态,当服务器端接收到这个包时,也进入 ESTABLISHED 状态,TCP 握手结束。

问题一:为什么需要三次握手呢?

在谢希仁著的《计算机网络》里说,『为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误』。怎么理解呢,我们假设一种情况,有一个建立连接的第一次握手的报文段因为滞留到网络中过了较长时间才发送到服务端。这时服务器是要做ACK应答的,如果只有两次握手就代表连接建立,那服务器此时就要等待客户端发送建立连接之后的数据。而这只是一个因滞留而废弃的请求,是不是白白浪费了很多服务器资源。

从另一个角度看这个问题,TCP是全双工的通信模式,需要保证两端都已经建立可靠有效的连接。在三次握手过程中,我们可以确认的状态是:

第一次握手:服务器确认自己接收OK,服务端确认客户端发送OK。

第二次握手:客户端确认自己发送OK,客户端确认自己接收OK,客户端确认服务器发送OK,客户端确认服务器接收OK。

第三次握手:服务器确认自己发送OK,服务器确认客户端接收OK。

只有握手三次才能达到全双工的目的:确认自己和对方都能够接收和发送消息。

TCP四次挥手

示意图如下:


四次挥手表示要发送四个包,挥手的目的是断开连接。

1、第一次挥手(FIN=1, seq=x)

假设客户端想要关闭连接,客户端发送一个 FIN 标志位置为1的包,表示自己已经没有数据可以发送了,但是仍然可以接受数据。

发送完毕后,客户端进入 FIN_WAIT_1 状态。

2、第二次挥手(ACK=1,ACKnum=x+1)

服务器端确认客户端的 FIN 包,发送一个确认包,表明自己接受到了客户端关闭连接的请求,但还没有准备好关闭连接。

发送完毕后,服务器端进入 CLOSE_WAIT 状态,客户端接收到这个确认包之后,进入 FIN_WAIT_2 状态,等待服务器端关闭连接。

3、第三次挥手(FIN=1,seq=y)

服务器端准备好关闭连接时,向客户端发送结束连接请求,FIN 置为1。

发送完毕后,服务器端进入 LAST_ACK 状态,等待来自客户端的最后一个ACK。

4、第四次挥手(ACK=1,ACKnum=y+1)

客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入 TIME_WAIT状态,等待可能出现的要求重传的 ACK 包。

服务器端接收到这个确认包之后,关闭连接,进入 CLOSED 状态。

客户端等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入 CLOSED 状态。

问题一:为什么挥手需要四次呢?为什么不能将ACK和FIN报文一起发送?

当服务器收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端『你发的FIN我收到了』。只有等到服务端所有的报文都发送完了,才能发FIN报文,所以要将ACK和FIN分开发送,这就导致需要四次挥手。

问题二:为什么TIMED_WAIT之后要等2MSL才进入CLOSED状态?

MSL是TCP报文的最大生命周期,因为TIME_WAIT持续在2MSL就可以保证在两个传输方向上的尚未接收到或者迟到的报文段已经消失,同时也是在理论上保证最后一个报文可靠到达。假设最后一个ACK丢失,那么服务器会再重发一个FIN,这是虽然客户端的进程不在了,但是TCP连接还在,仍然可以重发LAST_ACK。

HTTPS的流程

HTTPS = HTTP + TLS/SSL,它使用的端口默认为443,它的建立可以用下图表示:



1、客户端首次请求服务器,告诉服务器自己支持的协议版本,支持的加密算法及压缩算法,并生成一个随机数(client random)告知服务器。

2、服务器确认双方使用的加密方法,并返回给客户端证书以及一个服务器生成的随机数(server random)

3、客户端收到证书后,首先验证证书的有效性,然后生成一个新的随机数(premaster secret),并使用数字证书中的公钥,加密这个随机数,发送给服务器。

4、服务器接收到加密后的随机数后,使用私钥进行解密,获取这个随机数(premaster secret

5、服务器和客户端根据约定的加密方法,使用前面的三个随机数(client random, server random, premaster secret),生成『对话密钥』(session key),用来加密接下来的整个对话过程(对称加密)。

问题一:为什么握手过程需要三个随机数,而且安全性只取决于第三个随机数?

前两个随机数是明文传输,存在被拦截的风险,第三个随机数是通过证书公钥加密的,只有它是经过加密的,所以它保证了整个流程的安全性。前两个随机数的目的是为了保证最终对话密钥的『更加随机性』。

问题二:Charles如何实现HTTPS的拦截?

Charles要实现对https的拦截,需要在客户端安装Charles的证书并信任它,然后Charles扮演中间人,在客户端面前充当服务器,在服务器面前充当客户端。

问题三:为什么有些HTTPS请求(例如微信)抓包结果仍是加密的,如何实现的?


我在聊天过程中并没有抓到会话的请求,在小程序启动的时候到是抓到了一个加密内容。我手动触发该链接会下载一个加密文件,我猜测这种加密是内容层面的加密,它的解密是由客户端完成的,而不是在HTTPS建立过程完成的。

另外在研究这个问题的过程中,又发现了一些有趣的问题:

1、图中所示的三个https请求分别对应三个不同类型的图标,它们分别代表什么意思呢?

感谢iOS憨憨的回答。 第一个图标含义是HTTP/2.0,第二个图标含义是HTTP/1.1,第三个图标加锁是因为我用charles只抓取了443端口的请求,该请求端口为5228,所以不可访问。

2、第三个请求https://mtalk.google.com:5228图标和请求内容都加了锁,这个加锁是在https之上又加了一层锁吗?

这些问题暂时没有确切的答案,希望了解的小伙伴告知一下哈。

DNS解析流程

DNS(Domain name system)域名系统。DNS是因特网上作为域名和IP地址相互映射的一个分布式数据库,能够使用户通过域名访问到对应的服务器(IP地址)。具体的解析流程是这样的:

1、浏览器中输入想要访问的网站域名,操作系统会检查本地hosts文件是否有这个网址的映射关系,如果有就调用这个IP地址映射,完成域名解析。没有的话就走第二步。

2、客户端回向本地DNS服务器发起查询,如果本地DNS服务器收到请求,并可以在本地配置区域资源中查到该域名,就将对应结果返回为给客户端。如果没有就走第三步。

3、根据本地DNS服务器的设置,采用递归或者迭代查询,直至解析完成。

其中递归查询和迭代查询可以用如下两图表示。

递归查询

如图所示,递归查询是由DNS服务器一级一级查询传递的。


迭代查询

如果所示,迭代查询是找到指定DNS服务器,由客户端发起查询。



DNS劫持

DNS劫持发生在DNS服务器上,当客户端请求解析域名时将其导向错误的服务器(IP)地址。

常见的解决办法是使用自己的解析服务器或者是将域名以IP地址的方式发出去以绕过DNS解析。

Cookie和Session的区别

HTTP 是无状态协议,说明它不能以状态来区分和管理请求和响应。也就是说,服务器单从网络连接上无从知道客户身份。

可是怎么办呢?就给客户端们颁发一个通行证吧,每人一个,无论谁访问都必须携带自己通行证。这样服务器就能从通行证上确认客户身份了。这就是Cookie的工作原理。

  • Cookie:Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,实际上Cookie是服务器在本地机器上存储的一小段文本,并随着每次请求发送到服务器。Cookie技术通过请求和响应报文中写入Cookie信息来控制客户端的状态。

  • Session:Session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构来保存信息。当有用户请求创建一个session时,服务器会先检查这个客户端里是否已经包含了一个Session标识(session id),如果有就通过session id把session检索出来。如果没有就创建一个对应此Session的session id。这个session id会在本次响应中返回给客户端。

两者有以下区别:

1、存储位置:Cookie存放在客户端上,Session数据存放在服务器上。

2、Session 的运行依赖 session id,而 session id 是存在 Cookie 中的,也就是说,如果浏览器禁用了 Cookie ,同时 Session 也会失效

3、安全性:Cookie存在浏览器中,可能会被一些程序复制,篡改;而Session存在服务器相对安全很多。

4、性能:Session会在一定时间内保存在服务器上,当访问增多,会对服务器造成一定的压力。考虑到减轻服务器压力,应当使用Cookie

CDN是干什么用的

CDN(Content Delivery Network),根本作用是将网站的内容发布到最接近用户的网络『边缘』,以提高用户访问速度。概括的来说:CDN = 镜像(Mirror) + 缓存(Cache) + 整体负载均衡(GSLB)。

目前CDN都以缓存网站中的静态数据为主,如CSS、JS、图片和静态网页等数据。用户在从主站服务器请求到动态内容后再从CDN上下载这些静态数据,从而加速网页数据内容的下载速度,如淘宝有90%以上的数据都是由CDN来提供的。

CDN工作流程

一个用户访问某个静态文件(如CSS),这个静态文件的域名假如是http://www.baidu.com,而这个域名最终会被指向CDN全局中CDN负载均衡服务器,再由这个负载均衡服务器来最终分配是哪个地方的访问用户,返回给离这个访问用户最近的CDN节点。之后用户就直接去这个CDN节点访问这个静态文件了,如果这个节点中请求的文件不存在,就会再回到源站去获取这个文件,然后再返回给用户。


Socket的作用

socket位于应用层和传输层之间:


它的作用是为了应用层能够更方便的将数据经由传输层来传输。所以它的本质就是对TCP/IP的封装,然后应用程序直接调用socket API即可进行通信。上文中说的三次握手和四次挥手即是通过socket完成的。

我们可以从iOS中网络库分层找到BSD Sockets,它是位于CFNetwork之下。在CFNetwork中还有一个CFSocket,推测是对BSD Sockets的封装。


WebRTC是干什么用的

WebRTC

是一个可以用在视频聊天,音频聊天或P2P文件分享等Web App中的 API。借助WebRTC,你可以在基于开放标准的应用程序中添加实时通信功能。它支持在同级之间发送视频,语音和通用数据,从而使开发人员能够构建功能强大的语音和视频通信解决方案。该技术可在所有现代浏览器以及所有主要平台的本机客户端上使用。WebRTC项目是开源的并得到Apple,Google,Microsoft和Mozilla等的支持。

如果某一请求只在某一地特定时刻失败率较高,会有哪些原因

这个是某公司二面时的问题,是一个开放性问题,我总结了以下几点可能:

1、该时刻请求量过大

2、该地的网络节点较不稳定

3、用户行为习惯,比如该时刻为上班高峰期,或者某个群体的特定习惯



作者:iOS鑫
链接:https://www.jianshu.com/p/6b16f40d7354



收起阅读 »

一个用于Android 应用组件化时各组件的Application进行解耦的轻便型框架。

Lobster一个用于Android 应用组件化时各组件的Application进行解耦的轻便型框架。 三个注解即可搞定!一、功能介绍1.在组件中不使用BaseApplication实例,通过注解,直接使用组件自己创建的Application实例; 2.组件中...
继续阅读 »


Lobster

一个用于Android 应用组件化时各组件的Application进行解耦的轻便型框架。 三个注解即可搞定!

一、功能介绍

1.在组件中不使用BaseApplication实例,通过注解,直接使用组件自己创建的Application实例;
2.组件中自己创建的Application生命周期方法伴随壳子工程Application生命周期调用而调用;
3.组件中自己创建的Application可以配置优先级,用于优先或延后执行。

二、应用场景

组件化框架中,各组件有时需要持有Application的实例,但很多做法是在公共库中创建BaseApplication,  
让壳子工程的Application去继承BaseApplication,进而组件去持有BaseApplication的实例达到使用的目的,
然而这样会加剧组件对公共库的过分依赖,项目较大时,就会造成一定的耦合,可能会出现改一处而动全身的场景。
因此,在组件化当中,各组件应该像一个应用一样维护一个自己的Application,使用时拿的是自己Application的实例,
与其他组件隔离,也与公共库隔离,降低耦合!

三、使用方式

1.需要在壳子工程和其他module中添加如下依赖:

android {
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [LOBSTER_MODULE_NAME: project.getName()]
}
}
}
}

dependencies {
implementation project(path: ':lobster-annotation')
annotationProcessor project(path: ':lobster-compiler')
...
}

2.在壳子工程和其他Module中的Application中添加注解: ShellApp注解作用于壳子工程(主工程)Application,一般来说只有一个,ModuleApp注解作用于组件Application,可以设置优先级,AppInstance注解作用于组件Application的实例。

// 壳子工程的Application
@ShellApp
public class MyApplication extends Application {

@Override
public void onCreate() {
super.onCreate();
}
}
// 其他Module的Application
@ModuleApp(priority = 1)
public class Module1App extends Application {
private static final String TAG = "Lobster";

@AppInstance
public static Application mApplication1;

@Override
public void onCreate() {
super.onCreate();
Log.i(TAG , "Module1App->onCreate");
}
}
// 其他Module的Application
@ModuleApp(priority = 2)
public class Module2App extends Application {
private static final String TAG = "Lobster";
@AppInstance
public static Application mApplication2;

@Override
public void onCreate() {
super.onCreate();
Log.i(TAG , "Module2App->onCreate");
Toast.makeText(mApplication2, "I come from Module2App", Toast.LENGTH_SHORT).show();
}
}

3.没有了,可以开始耍了!

代码下载:Lobster.zip

收起阅读 »

Android自定义搜索控件 KSearchView

KSearchView自定义搜索控件布局示例代码 <com.kacent.widget.view.KingSearchView android:id="@+id/search_view" android:layout_wi...
继续阅读 »

KSearchView

自定义搜索控件


布局示例代码

 <com.kacent.widget.view.KingSearchView
android:id="@+id/search_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
<!-提示文本->
app:hint_text="输入搜索内容"
<!-icon的padding->
app:icon_padding_bottom="5dp"
<!-searchView输入框的padding->
app:search_padding_bottom="10dp"
app:search_padding_start="30dp"
app:search_padding_top="10dp"
<!-searchView背景设置->
app:search_view_background="@drawable/my_search_shape"
app:text_size="8sp" />

设置搜索监听器

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val searchView = findViewById<KingSearchView>(R.id.search_view)
searchView.setQueryListener(object : KingSearchView.OnQueryListener {
override fun onQuery(value: String) {
if (TextUtils.isEmpty(value)) {
Toast.makeText(this@MainActivity, "没有输入相关搜索内容", Toast.LENGTH_SHORT).show()
}
Log.e("搜索内容", value)
}
})
}
}


代码下载:KingSearchView-master.zip

收起阅读 »

Android基础到进阶UI爸爸级TextView介绍+实例

TextView是什么 向用户显示文本,并可选择允许他们编辑文本。TextView是一个完整的文本编辑器,但是基类为不允许编辑;其子类EditText允许文本编辑。 咱们先上一个图看看TextView的继承关系: 从上图可以看出TxtView继承了Vi...
继续阅读 »

TextView是什么


向用户显示文本,并可选择允许他们编辑文本。TextView是一个完整的文本编辑器,但是基类为不允许编辑;其子类EditText允许文本编辑。


咱们先上一个图看看TextView的继承关系:


从上图可以看出TxtView继承了View,它还是Button、EditText等多个组件类的父类。咱们看看这些子类是干嘛的。



  • Button:用户可以点击或单击以执行操作的用户界面元素。

  • CheckedTextView:TextView支持Checkable界面和显示的扩展。

  • Chronometer:实现简单计时器的类。

  • DigitalClock:API17已弃用可用TextClock替代。

  • EditText:用于输入和修改文本的用户界面元素。

  • TextClock:可以将当前日期和/或时间显示为格式化字符串。


看看他的儿子都这么牛掰,何况是爸爸,今天咱就看看这个爸爸级组件:TextView


使用TextView


1.在xml中创建并设置属性



咱们看上图说话。上图的文字显示多种多样,但是也仅包含TextView的部分功能,看看这多种多样的显示也是比较有意思的。


下面咱看看代码实践:


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="@dimen/dimen_20"
    android:orientation="vertical">

    <!--在Design中表示可从左侧控件展示处拖拽至布局文件上,创建简单一个TextView。-->
    <TextView
        android:id="@+id/textView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="TextView" />

    <!--修改颜色、大小-->
    <!--设置颜色 @color/color_ff0000位置:app/values/colors-->
    <!--设置大小 @dimen/text_size_18位置:app/values/dimens-->
    <!--设置内容 @string/str_setting_color_size位置:app/values/strings-->
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/str_setting_color_size"
        android:layout_marginTop="@dimen/dimen_10"
        android:textColor="@color/color_ff0000"
        android:textSize="@dimen/text_size_20" />

    <!--添加图片和使用阴影-->
    <!--添加图片:drawableTop、drawableBottom、drawableLeft(drawableStart)、drawableRight(drawableEnd)-->
    <!--使用阴影:shadowColor(阴影颜色)、shadowDx(tv_2位置为基准,数字越大越往右)、
    shadowDy(tv_2位置为基准,数字越大越往下)、shadowRadius(数字越大越模糊)-->

    <!--图片 @mipmap/ic_launcher 位置:app/mipmap/任意一个目录能找到即可-->
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:drawableLeft="@mipmap/ic_launcher"
        android:layout_marginTop="@dimen/dimen_10"
        android:gravity="center_vertical"
        android:shadowColor="@color/color_FF773D"
        android:shadowDx="30"
        android:shadowDy="-20"
        android:shadowRadius="2"
        android:text="右侧添加图片和使用阴影"
        android:textColor="@color/color_188FFF"
        android:textSize="@dimen/text_size_20" />

    <!--对电话和邮件增加链接-->
    <!--autoLink对文本内容自动添加E-mail地址、电话号码添加超级链接-->
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:autoLink="email|phone"
        android:gravity="center_vertical"
        android:layout_marginTop="@dimen/dimen_10"
        android:text="可点击跳转邮件:SCC5201314@qq.com\n可点击跳转电话:0215201314"
        android:textColor="@color/color_188FFF"
        android:textSize="@dimen/text_size_14" />

    <!--内容过多-->
    <!--maxLength最多显示几行,单行也可用android:singleline="true"-->
    <!--ellipsize,内容显示不下时,显示...(位置最前、中间、最后都可以),这里要加行数限制才行-->
    <!--lineSpacingMultiplier,行距-->
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:ellipsize="end"
        android:gravity="center_vertical"
        android:lineSpacingMultiplier="1.2"
        android:layout_marginTop="@dimen/dimen_10"
        android:maxLength="2"
        android:text="TxtView继承了View,它还是Button、EditText两个UI组件类的父类。它的作用是在用户界面上显示文本素。从功能上来看TextView就是个文本编辑器,只不过Android关闭的它的可编辑功能。如果需要一个可编辑的文本框,就要使用到它的子类Editext了,Editext允许用户编辑文本框中的内容。TextView和Editext它俩最大的区别就在于TextView不允许用户编辑文本内容,Editext允许用户编辑文本内容。
下面咱写几个实例来详细了解一下TextView的。"

        android:textColor="@color/color_188FFF"
        android:textSize="@dimen/text_size_14" />

    <!--background设置背景色-->
    <!--padding内边距(边到可用范围的距离)-->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/color_ff0000"
        android:layout_marginTop="@dimen/dimen_10"
        android:padding="10dp"
        android:text="背景色红色的文本"
        android:textColor="@color/white" />


    <!--带边框的文本-->
    <!--layout_margin外边距(TextView到其他控件的距离)-->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/dimen_10"
        android:background="@drawable/bg_tv_frame_red"
        android:padding="10dp"
        android:text="带着红色边框的文本" />

    <!--带边框的文本背景色渐变-->
    <!--代码可实现文本的渐变-->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/dimen_10"
        android:background="@drawable/bg_tv_frame_gradient"
        android:padding="10dp"
        android:textColor="@color/white"
        android:text="带着边框和背景色渐变的文本" />

    
</LinearLayout>

background设置边框的文件 android:background="@drawable/bg_tv_frame_red"


<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <!--radius四个圆角统一设置,也可以单独对某一个圆角设置。例:topLeftRadius-->
    <corners android:radius="2dp"/>
    <!--边框宽度width、颜色color-->
    <stroke android:width="4px" android:color="@color/color_ff0000" />
</shape>

带着边框和背景色渐变 android:background="@drawable/bg_tv_frame_gradient"


<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <!--radius四个圆角统一设置,也可以单独对某一个圆角设置。例:topLeftRadius-->
    <corners android:radius="8dp"/>
    <!--边框宽度width、颜色color-->
    <stroke android:width="1dp" android:color="@color/color_ff0000" />
    <!--渐变的颜色设置开始到结束-->
    <gradient
        android:startColor="@color/color_188FFF"
        android:centerColor="@color/color_FF773D"
        android:endColor="@color/color_ff0000"
        android:type="linear"
        />

</shape>

2.在xml中创建,在代码中设置属性




  • 布局文件


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:layout_margin="@dimen/dimen_20"
   android:orientation="vertical">
   <TextView
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:text="下面是用代码实现效果"
       android:textSize="@dimen/text_size_18"
       android:layout_marginTop="@dimen/dimen_20"
       android:layout_marginBottom="@dimen/dimen_10"
       android:textColor="@color/black"
       android:textStyle="bold" />

   <TextView
       android:id="@+id/tv_flag"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:textColor="@color/color_188FFF"
       android:layout_marginTop="@dimen/dimen_10"
       android:text="给文本加划线"
       android:textSize="@dimen/text_size_18" />

   <TextView
       android:id="@+id/tv_gradient"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:layout_marginTop="@dimen/dimen_10"
       android:textColor="@color/white"
       android:text="文字渐变是不是很神奇"
       android:textSize="@dimen/text_size_18" />

   <TextView
       android:id="@+id/tv_bg"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_marginTop="@dimen/dimen_10"
       android:padding="10dp"
       android:text="设置背景色"
       android:textColor="@color/white"
       android:textSize="@dimen/text_size_18" />

   <TextView
       android:id="@+id/tv_size"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:layout_marginTop="@dimen/dimen_10"
       android:textColor="@color/color_ff0000"
       android:text="文字特别大小不一致" />

   <TextView
       android:id="@+id/tv_onclick"
       android:layout_width="match_parent"
       android:layout_marginTop="@dimen/dimen_10"
       android:layout_height="wrap_content"
       android:textSize="@dimen/dimen_20"
       android:text="可点击可长按" />
</LinearLayout>


  • 运行结果




  • 在代码中实现


        //下划线并加清晰
        tv_flag.getPaint().setFlags(Paint.UNDERLINE_TEXT_FLAG | Paint.ANTI_ALIAS_FLAG);
        tv_flag.getPaint().setAntiAlias(true);//抗锯齿

        int[] colors = {0xff188fff0xffff773D0xffff0000};//颜色的数组
        LinearGradient mLinearGradient = new LinearGradient(000
                tv_gradient.getPaint().getTextSize(), colors, null, Shader.TileMode.CLAMP);
        tv_gradient.getPaint().setShader(mLinearGradient);
        tv_gradient.invalidate();

        int fillColor = Color.parseColor("#ff0000");//内部填充颜色
        GradientDrawable gd = new GradientDrawable();//创建drawable
        gd.setColor(fillColor);//设置背景色
        gd.setCornerRadius(10);//设置圆角
        tv_bg.setBackground(gd);//设置背景

        Spannable wordtoSpan = new SpannableString(tv_size.getText().toString());
        //setSpan:参数1,设置文字大小;参数2,开始的文字位置;参数3,结束改变文字位置不包含这个位置
        wordtoSpan.setSpan(new AbsoluteSizeSpan(DensityUtil.dip2px(this18)), 02, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        wordtoSpan.setSpan(new AbsoluteSizeSpan(DensityUtil.dip2px(this24)), 25, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        wordtoSpan.setSpan(new AbsoluteSizeSpan(DensityUtil.dip2px(this10)), 5, tv_size.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        tv_size.setText(wordtoSpan);

        //TextView其实也是有点击事件的毕竟它的爸爸Veiew
        tv_onclick.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                MLog.e("这里是点击事件");
                Toast.makeText(TextViewActivity.this,"这里是点击事件",Toast.LENGTH_SHORT).show();
            }
        });
        tv_onclick.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                MLog.e("这里长按事件");
                Toast.makeText(TextViewActivity.this,"这里长按事件",Toast.LENGTH_SHORT).show();
                //true表示事件已消费
                return true;
            }
        });


  • 运行结果分析



    • TextView的属性在xml中可以使用的大部分在代码中也是可以实现的,看个人喜好怎么去使用。

    • 因TextView继承View,所以可以使用View的方法。如View.OnClickListener()和View.OnLongClickListener()还有去慢慢探索吧。



3.在代码中创建并设置属性



  • 先看效果图:




  • 下面是实现所用的代码:


  //ll_act_tv布局文件根布局id
  LinearLayout ll_act_tv = findViewById(R.id.ll_act_tv);
  TextView textView = new TextView(this);//创建控件
  textView.setText("蠢代码写的哦");//设置控件内容
  textView.setTextColor(Color.RED);//设置控件颜色
  textView.setTextSize(DensityUtil.dip2px(this20));//设置控件字体大小
  ll_act_tv.addView(textView);

TextView今天就聊到这里,后面还有它的子类,比较子类也是比较厉害的不可能一文搞定。你学会了吗?嘿嘿嘿


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

Android-自定义拼图验证码

废话不多说,先上图: 从效果图开始"临摹" 分析 从上面的效果图中,我们可以很直观的看出一共包含三个元素:背景图、空缺部分、填充部分,需要注意的是: 1. 空缺部分缺失的图片刚好是填充部分 2. 我们把填充部分位置固定在左侧,而随机生成空缺部分在右侧...
继续阅读 »

废话不多说,先上图:


1.gif


从效果图开始"临摹"


分析


从上面的效果图中,我们可以很直观的看出一共包含三个元素:背景图、空缺部分、填充部分,需要注意的是:
1. 空缺部分缺失的图片刚好是填充部分
2. 我们把填充部分位置固定在左侧,而随机生成空缺部分在右侧,增加验证难度


思路



  1. 准备背景图片,通过canvas.drawBitmap() 方法画出背景图

  2. 计算View宽高,随机生成空缺部分的x坐标在(width/3, width)范围,固定填充部分的x左边在(0,width/3)范围内,保证填充部分和空缺部分在初始化时没有重叠。(不严谨,具体数值还要结合空缺部分/填充部分尺寸详细计算,仅提供思路)。

  3. 先随机生成空缺部分,然后根据空缺部分在原来Bitmap上的左边生成一样大小一样形状的图片,用于填充部分。

  4. 然后重写onTouchEvent方法,处理拖动时填充部分的位移,在MotionEvent.ACTION_UP条件下,计算填充部分和空缺部分在画布中的x坐标差值,判断当差值小于阙值 dx 时,则认为通过验证,否则调用 invalidate() 方法重新生成验证码。


主要代码分析


这里重写了onMeasure方法,根据我们准备的原图片尺寸设置View宽高,并且重新生成和View一样尺寸的背景图newBgBitmap,统一尺寸以便后面我们对左边的转化。(这里曾经有些地方参照画布尺寸计算,有些地方参照背景图bitmap尺寸计算,导致填充部分和空缺部分没有吻合)。


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int minimumWidth = getSuggestedMinimumWidth();
/*根据原背景图宽高比设置画布尺寸*/
width = measureSize(minimumWidth, widthMeasureSpec);
float scale = width / (float) bgBitmap.getWidth();
height = (int) (bgBitmap.getHeight() * scale);
setMeasuredDimension(width, height);

/*根据画布尺寸生成相同尺寸的背景图*/
newBgBitmap = clipBitmap(bgBitmap, width, height);
/*根据新的背景图生成填充部分*/
srcBitmap = createSmallBitmap(newBgBitmap);
}


设置画笔的混合模式,生成一张自定义形状的图片供填充部分使用


    public Bitmap createSmallBitmap(Bitmap var) {
Bitmap bitmap = Bitmap.createBitmap(shadowSize, shadowSize, Bitmap.Config.ARGB_8888);
Canvas canvas1 = new Canvas(bitmap);
canvas1.drawCircle(shadowSize / 2, shadowSize / 2, shadowSize / 2, paintSrc);
/*设置混合模式*/
paintSrc.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

/*在指定范围随机生成空缺部分坐标,保证空缺部分出现在View右侧*/
int min = width / 3;
int max = width - shadowSize / 2 - padding;
Random random = new Random();
shadowLeft = random.nextInt(max) % (max - min + 1) + min;
Rect rect = new Rect(shadowLeft, (height - shadowSize) / 2, shadowSize + shadowLeft, (height + shadowSize) / 2);
RectF rectF = new RectF(0, 0, shadowSize, shadowSize);
canvas1.drawBitmap(var, rect, rectF, paintSrc);
paintSrc.setXfermode(null);
return bitmap;
}

在onDraw()方法中依次画出背景图、空缺部分、填充部分,注意先后顺序(具体细节自行处理,例如阴影、凹凸感等等)


    @Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
RectF rectF = new RectF(0, 0, width, height);
/*画背景图*/
canvas.drawBitmap(newBgBitmap, null, rectF, paintSrc);

bgPaint.setColor(Color.parseColor("#000000"));
/*画空缺部分周围阴影*/
canvas.drawCircle(shadowLeft + shadowSize / 2, height / 2, shadowSize / 2, bgPaint);
/*画空缺部分*/
canvas.drawCircle(shadowLeft + shadowSize / 2, height / 2, shadowSize / 2, paintShadow);

Rect rect = new Rect(srcLeft, (height - shadowSize) / 2, shadowSize + srcLeft, (height + shadowSize) / 2);

bgPaint.setColor(Color.parseColor("#FFFFFF"));
/*画填充部分周围阴影*/
canvas.drawCircle(srcLeft + shadowSize / 2, height / 2, shadowSize / 2, bgPaint);
/*画填充部分*/
canvas.drawBitmap(srcBitmap, null, rect, paintSrc);
}

草纸代码参考


随写随发布?


package com.example.qingfengwei.myapplication;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BlurMaskFilter;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Toast;

import java.util.Random;


public class SlidingVerificationView extends View {

private Bitmap bgBitmap;
private Bitmap newBgBitmap;
private Bitmap srcBitmap;

private Paint paintShadow;
private Paint paintSrc;
private float curX;
private float lastX;

private int dx;
private int shadowSize = dp2px(60);
private int padding = dp2px(40);
private int shadowLeft;
private int srcLeft = padding;

private int width, height;

private Paint bgPaint;

private OnVerifyListener listener;

public SlidingVerificationView(Context context) {
this(context, null);
}

public SlidingVerificationView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public SlidingVerificationView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
paintShadow = new Paint();
paintShadow.setAntiAlias(true);
paintShadow.setColor(Color.parseColor("#AA000000"));


paintSrc = new Paint();
paintSrc.setAntiAlias(true);
paintSrc.setFilterBitmap(true);
paintSrc.setStyle(Paint.Style.FILL_AND_STROKE);
paintSrc.setColor(Color.WHITE);

bgPaint = new Paint();
bgPaint.setMaskFilter(new BlurMaskFilter(5, BlurMaskFilter.Blur.OUTER));
bgPaint.setAntiAlias(true);
bgPaint.setStyle(Paint.Style.FILL);

bgBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.syzt);
}

public void setVerifyListener(OnVerifyListener listener) {
this.listener = listener;
}

public Bitmap clipBitmap(Bitmap bm, int newWidth, int newHeight) {
int width = bm.getWidth();
int height = bm.getHeight();
float scaleWidth = ((float) newWidth) / width;
float scaleHeight = ((float) newHeight) / height;
Matrix matrix = new Matrix();
matrix.postScale(scaleWidth, scaleHeight);
return Bitmap.createBitmap(bm, 0, 0, width, height, matrix, true);
}


public Bitmap createSmallBitmap(Bitmap var) {
Bitmap bitmap = Bitmap.createBitmap(shadowSize, shadowSize, Bitmap.Config.ARGB_8888);
Canvas canvas1 = new Canvas(bitmap);
canvas1.drawCircle(shadowSize / 2, shadowSize / 2, shadowSize / 2, paintSrc);
/*设置混合模式*/
paintSrc.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));


/*在指定范围随机生成空缺部分坐标,保证空缺部分出现在View右侧*/
int min = width / 3;
int max = width - shadowSize / 2 - padding;
Random random = new Random();
shadowLeft = random.nextInt(max) % (max - min + 1) + min;
Rect rect = new Rect(shadowLeft, (height - shadowSize) / 2, shadowSize + shadowLeft, (height + shadowSize) / 2);
RectF rectF = new RectF(0, 0, shadowSize, shadowSize);
canvas1.drawBitmap(var, rect, rectF, paintSrc);
paintSrc.setXfermode(null);
return bitmap;
}


@Override
public boolean onTouchEvent(MotionEvent event) {
curX = event.getRawX();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = event.getRawX();
break;
case MotionEvent.ACTION_MOVE:
dx = (int) (curX - lastX);
srcLeft = dx + padding;
invalidate();
break;
case MotionEvent.ACTION_UP:

boolean isSuccess = Math.abs(srcLeft - shadowLeft) < 8;

if (isSuccess) {
Toast.makeText(getContext(), "验证成功!", Toast.LENGTH_SHORT).show();
Log.d("w", "check success!");
} else {
Toast.makeText(getContext(), "验证失败!", Toast.LENGTH_SHORT).show();
Log.d("w", "check fail!");
srcBitmap = createSmallBitmap(newBgBitmap);
srcLeft = padding;
invalidate();
}

if (listener != null) {
listener.onResult(isSuccess);
}
break;
}

return true;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int minimumWidth = getSuggestedMinimumWidth();
/*根据原背景图宽高比设置画布尺寸*/
width = measureSize(minimumWidth, widthMeasureSpec);
float scale = width / (float) bgBitmap.getWidth();
height = (int) (bgBitmap.getHeight() * scale);
setMeasuredDimension(width, height);

/*根据画布尺寸生成相同尺寸的背景图*/
newBgBitmap = clipBitmap(bgBitmap, width, height);
/*根据新的背景图生成填充部分*/
srcBitmap = createSmallBitmap(newBgBitmap);

}

private int measureSize(int defaultSize, int measureSpec) {
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
int result = defaultSize;
switch (mode) {
case MeasureSpec.UNSPECIFIED:
result = defaultSize;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = size;
break;
}
return result;
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
RectF rectF = new RectF(0, 0, width, height);
/*画背景图*/
canvas.drawBitmap(newBgBitmap, null, rectF, paintSrc);

bgPaint.setColor(Color.parseColor("#000000"));
/*画空缺部分周围阴影*/
canvas.drawCircle(shadowLeft + shadowSize / 2, height / 2, shadowSize / 2, bgPaint);
/*画空缺部分*/
canvas.drawCircle(shadowLeft + shadowSize / 2, height / 2, shadowSize / 2, paintShadow);

Rect rect = new Rect(srcLeft, (height - shadowSize) / 2, shadowSize + srcLeft, (height + shadowSize) / 2);

bgPaint.setColor(Color.parseColor("#FFFFFF"));
/*画填充部分周围阴影*/
canvas.drawCircle(srcLeft + shadowSize / 2, height / 2, shadowSize / 2, bgPaint);
/*画填充部分*/
canvas.drawBitmap(srcBitmap, null, rect, paintSrc);
}

public static int dp2px(float dp) {
float density = Resources.getSystem().getDisplayMetrics().density;
return (int) (density * dp + 0.5f);
}
}



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

手把手教你搭建AndroidJenkins环境及一键自动构建打包

前言: 之前看到后端的同事,在服务器上刷刷的敲命令觉得很酷,给我的感觉是Linux很难上手。自从公司给我配了台服务器后,操作了一下,感觉和想的不是那么一回事,所以还是得多动手,不要空想。 正文开始: 看到网上说Linux安装软件基本是用yum,所以这边也是...
继续阅读 »

前言: 之前看到后端的同事,在服务器上刷刷的敲命令觉得很酷,给我的感觉是Linux很难上手。自从公司给我配了台服务器后,操作了一下,感觉和想的不是那么一回事,所以还是得多动手,不要空想。


正文开始:


看到网上说Linux安装软件基本是用yum,所以这边也是使用yum,从0-1搭建Linux Jenkins,实现Android 一键自动化构建部署打包。


步骤一:安装JDK环境


1.查看jdk的版本:


# yum -y list java*


2.选择要安装的JDK版本(带devel是JDK)


# yum install -y java-1.8.0-openjdk-devel.x86_64


3.安装完后,查看是否安装成功(看到版本号表示安装成功,不需要像Windows配置java_home环境变量)


# java -version


image.png


步骤二:安装Jenkins


1.yum安装Jenkins


# yum install jenkins


如果没有Jenkins的repos,按照官网提示再操作:


# sudo wget -O /etc/yum.repos.d/jenkins.repo  https://pkg.jenkins.io/redhat-stable/jenkins.repo
# sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key

2.修改Jenkins默认的配置(怕端口冲突,改成你想要名字和端口):


# cd /etc/sysconfig/
# vi jenkins

image.png


3.启动Jenkins


# service jenkins start


按照提示,到指定目录复制密码,下一步是选择安装默认的插件(推荐),注册一个账户,最后就会来到这个界面,表示Jenkins安装成功。


image.png


步骤三:安装gradle


1.安装gradle gradle.org/releases/


image.png 拿到安装包的下载链接: 创建一个安装的目录,我在新建了一个文件夹/opt/gradle/下 下载


# cd /opt
# mkdir gradle
# cd /opt/gradle
# wget https://downloads.gradle-dn.com/distributions/gradle-6.5-all.zip
# unzip /opt/gradle gradle-6.5-all.zip

步骤四:command tools 下载



  1. Command line tools only linux版本


developer.android.com/studio


# cd /opt
# mkdir android
# cd /opt/android
# wget https://dl.google.com/android/repository/commandlinetools-linux-7302050_latest.zip
# unzip /opt/android commandlinetools-linux-7302050_latest.zip

步骤五:配置gradle和Android SDK 环境变量


# cd /etc
# /etc/profile

image.png 在最后追加环境变量,保存


export ANDROID_HOME="/opt/android"
export GRADLE_HOME="/opt/gradle"
export PATH="$ANDROID_HOME/tools:$ANDROID_HOME/tools/bin:$ANDROID_HOME/platform-tools:$GRADLE_HOME/gradle-4.9/bin:$PATH"


刷新当前的shell环境


# source /etc/profile


配置gradle全局代理


def repoConfig = {
all { ArtifactRepository repo ->
if (repo instanceof MavenArtifactRepository) {
def url = repo.url.toString()
if (url.contains('repo1.maven.org/maven2') || url.contains('jcenter.bintray.com')) {
println "gradle 初始化: (${repo.name}: ${repo.url}) 移除"
remove repo
}
}
}
maven { url 'https://maven.aliyun.com/repository/central' }
maven { url 'https://maven.aliyun.com/repository/public' }
maven { url 'https://maven.aliyun.com/repository/jcenter' }
maven { url 'https://maven.aliyun.com/repository/google' }
maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
mavenLocal()
mavenCentral()
}

allprojects {
buildscript {
repositories repoConfig
}

repositories repoConfig
}

这里因为项目里面配了一个google的maven地址,导致一直gradle构建超半个小时,排查了很久。


项目builid.gradle里面千万不要配google的maven地址
项目builid.gradle里面千万不要配google的maven地址
项目builid.gradle里面千万不要配google的maven地址
就是他,浪费了我几天时间排查,正常用
google()
jcenter()
就可以了

image.png


查看gradle是否配置成功
# gradle -version

image.png


步骤六:sdkmanager方式安装SDK


因为是sdkmanager访问的是google,所以配置了谷歌代理才可以访问,当时折腾了很久才找到的dl.google.com代理


# cd /etc/
# vi hosts

203.208.40.36 dl.google.com


image.png


# cd /opt/android/cmdline-tools


在里面创建一个latest,将文件夹里面文件全部放到latest里面。


# cd /opt/android/cmdline-tools/latest/bin


查看版本
# ./sdkmanager --list
安装想要的SDK版本
# ./sdkmanager --install "platforms;android-29"
# ./sdkmanager --install "build-tools;29.0.2"

步骤六:安装git


1.下载最新的git github.com/git/git/rel…


2.我通过xftp copy在/root目录下,解压


安装依赖库
yum install curl-devel expat-devel gettext-devel openssl-devel zlib-devel gcc perl-ExtUtils-MakeMaker

# cd /root/git-2.32.0
make prefix=/usr/local all
make prefix=/usr/local install

最后测试下git clone 你的git地址试试能不能拉代码

步骤七:最后一步,Jenkins上创建项目


1.在plugins.jenkins.io/ 下载插件


构建化插件:extended-choice-parameter


image.png


点击高级-上传插件-等待安装成功即可


image.png


2.新建任务


image.png


构建化参数添加参考


image.png


我这个教程,Jenkins里面是零配置,直接执行shell脚本,很方便


具体的脚本自己根据实际需求来编写,我这里就不举例了。


gradle clean
gradle assembleRelease--stacktrace -info

结语: 搭建Jenkins的服务,我深有体会,由于没有服务器,首先在自己电脑搭建了一套Windows的Jenkins,也是遇到了各种疑难问题,最后还是解决了。然后,在Linux上部署这套Jenkins环境,就变得很轻松。这次最大的收获就是熟悉了Linux的操作,实现了Android 一键自动打包上传到服务器,减少了打包,上传服务器繁琐的操作。


如果本文对你有帮助,请帮忙对个赞或者留言,你的鼓励是我分享的动力。




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

Android BaseDialog(开发必备)动画、加载进度、阴影

GitHubAPK使用方法将libray模块复制到项目中,或者直接在build.gradle中依赖:allprojects { repositories { maven { url 'https://jitpack.io' } } } ...
继续阅读 »

GitHub

APK

使用方法

将libray模块复制到项目中,或者直接在build.gradle中依赖:

allprojects {
repositories {

maven { url 'https://jitpack.io' }
}
}
dependencies {
compile 'com.github.AnJiaoDe:BaseDialog:V1.1.8'
}

1.Center


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:background="@drawable/white_shape"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="80dp"
android:text="确定删除吗?"
android:textSize="16sp"
android:gravity="center"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/line"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="48dp"

android:text="取消"
android:id="@+id/tv_cancel"
android:gravity="center"
android:textSize="16sp"/>
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="@color/line"/>
<TextView
android:layout_width="0dp"
android:layout_weight="1"
android:id="@+id/tv_confirm"
android:layout_height="48dp"
android:text="确定"
android:gravity="center"

android:textSize="16sp"/>
</LinearLayout>
</LinearLayout>

2.Left


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="250dp"
android:layout_height="match_parent"
android:background="@color/white"

android:orientation="vertical">

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="Google Assistant: 一句 OK, Google,多少手指都用不上了
人工智能是今年的 Google I/O 的一大主题。在发布会一开始,Google CEO 桑达拉·皮蔡(Sundar Pichai)就强调机器学习在生活中扮演的重要角色。随后,一系列基于 Google 人工智能的产品纷至沓来。



OK, Google. 这句耳熟能详的命令,如今承载了 Google 全新的产品——Google Assistant.

之所以 Google Assistant 是发布会上首个亮相的产品,是因为后续登场的数个产品都基于这一技术。Google 用将近十年的时间,改善自己的语音识别技术,更强调自然语义和对话式搜索。"
android:textSize="16sp" />

</LinearLayout>

3.Top


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:orientation="vertical">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<TextView
android:id="@+id/tv_photo"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center"
android:text="拍照"
android:textSize="16sp" />

<View
android:layout_width="match_parent"
android:layout_height="0.1dp"
android:background="@color/line" />

<TextView

android:id="@+id/tv_album"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center"
android:text="从相册选择"

android:textSize="16sp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="10dp"
android:background="@color/bg"/>
<TextView
android:id="@+id/tv_photo_cancel"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center"
android:text="取消"

android:textSize="16sp" />
</LinearLayout>

4.Right


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="250dp"
android:layout_height="match_parent"
android:background="@color/white"

android:orientation="vertical">

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="Google Assistant: 一句 OK, Google,多少手指都用不上了
人工智能是今年的 Google I/O 的一大主题。在发布会一开始,Google CEO 桑达拉·皮蔡(Sundar Pichai)就强调机器学习在生活中扮演的重要角色。随后,一系列基于 Google 人工智能的产品纷至沓来。



OK, Google. 这句耳熟能详的命令,如今承载了 Google 全新的产品——Google Assistant.

之所以 Google Assistant 是发布会上首个亮相的产品,是因为后续登场的数个产品都基于这一技术。Google 用将近十年的时间,改善自己的语音识别技术,更强调自然语义和对话式搜索。"
android:textSize="16sp" />

</LinearLayout>

5.Bottom


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:orientation="vertical">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<TextView
android:id="@+id/tv_photo"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center"
android:text="拍照"
android:textSize="16sp" />

<View
android:layout_width="match_parent"
android:layout_height="0.1dp"
android:background="@color/line" />

<TextView

android:id="@+id/tv_album"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center"
android:text="从相册选择"

android:textSize="16sp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="10dp"
android:background="@color/bg"/>
<TextView
android:id="@+id/tv_photo_cancel"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center"
android:text="取消"

android:textSize="16sp" />
</LinearLayout>

6.Progress


public class MainActivity extends BaseActivity {
private BaseDialog dialog;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn_center).setOnClickListener(this);
findViewById(R.id.btn_left).setOnClickListener(this);
findViewById(R.id.btn_top).setOnClickListener(this);
findViewById(R.id.btn_right).setOnClickListener(this);
findViewById(R.id.btn_bottom).setOnClickListener(this);
findViewById(R.id.btn_progress).setOnClickListener(this);

}

@Override
public void onClick(View v) {

switch (v.getId()) {
case R.id.btn_center:
dialog = new BaseDialog(this);
dialog.contentView(R.layout.dialog_center)
.canceledOnTouchOutside(true).show();
dialog.findViewById(R.id.tv_confirm).setOnClickListener(this);
dialog.findViewById(R.id.tv_cancel).setOnClickListener(this);

break;
case R.id.btn_left:
BaseDialog dialog_left = new BaseDialog(this);

dialog_left.contentView(R.layout.dialog_left)
.layoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT))
.dimAmount(0.5f)
.gravity(Gravity.LEFT | Gravity.CENTER)
.animType(BaseDialog.AnimInType.LEFT)
.canceledOnTouchOutside(true).show();

break;
case R.id.btn_top:
BaseDialog dialog_top = new BaseDialog(this);

dialog_top.contentView(R.layout.dialog_photo)
.layoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT))
.dimAmount(0.5f)
.gravity(Gravity.TOP)
.offset(0, ScreenUtils.dpInt2px(this, 48))
.animType(BaseDialog.AnimInType.TOP)
.canceledOnTouchOutside(true).show();


break;
case R.id.btn_right:
BaseDialog dialog_right = new BaseDialog(this);

dialog_right.contentView(R.layout.dialog_right)
.layoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT))

.gravity(Gravity.RIGHT | Gravity.CENTER)
.animType(BaseDialog.AnimInType.RIGHT)
.offset(20, 0)
.canceledOnTouchOutside(true).show();

break;
case R.id.btn_bottom:
BaseDialog dialog_bottom = new BaseDialog(this);

dialog_bottom.contentView(R.layout.dialog_photo)
.gravity(Gravity.BOTTOM)
.animType(BaseDialog.AnimInType.BOTTOM)
.canceledOnTouchOutside(true).show();


break;
case R.id.btn_progress:

ProgressDialog progressDialog = new ProgressDialog(this);
progressDialog.color_iv(0xffffffff)
.color_bg_progress(0xffffffff)
.colors_progress(0xff2a5caa).show();
break;
case R.id.tv_confirm:
dialog.dismiss();
break;
case R.id.tv_cancel:
dialog.dismiss();
break;
}

}
}

源码:

BaseDialog

public class BaseDialog extends Dialog {

public BaseDialog(Context context) {
this(context, 0);

}


public BaseDialog(Context context, int themeResId) {
super(context, themeResId);

requestWindowFeature(Window.FEATURE_NO_TITLE);// 去除对话框的标题
GradientDrawable gradientDrawable = new GradientDrawable();
gradientDrawable.setColor(0x00000000);
getWindow().setBackgroundDrawable(gradientDrawable);//设置对话框边框背景,必须在代码中设置对话框背景,不然对话框背景是黑色的

dimAmount(0.2f);
}

public BaseDialog contentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
return this;
}


public BaseDialog contentView(@NonNull View view) {
getWindow().setContentView(view);
return this;
}

public BaseDialog contentView(@NonNull View view, @Nullable ViewGroup.LayoutParams params) {
getWindow().setContentView(view, params);
return this;
}
public BaseDialog layoutParams(@Nullable ViewGroup.LayoutParams params) {
getWindow().setLayout(params.width, params.height);
return this;
}


/**
* 点击外面是否能dissmiss
*
* @param canceledOnTouchOutside
* @return
*/
public BaseDialog canceledOnTouchOutside(boolean canceledOnTouchOutside) {
setCanceledOnTouchOutside(canceledOnTouchOutside);
return this;
}

/**
* 位置
*
* @param gravity
* @return
*/
public BaseDialog gravity(int gravity) {

getWindow().setGravity(gravity);

return this;

}

/**
* 偏移
*
* @param x
* @param y
* @return
*/
public BaseDialog offset(int x, int y) {
WindowManager.LayoutParams layoutParams = getWindow().getAttributes();
layoutParams.x = x;
layoutParams.y = y;

return this;
}

/*
设置背景阴影,必须setContentView之后调用才生效
*/
public BaseDialog dimAmount(float dimAmount) {

WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.dimAmount = dimAmount;
return this;
}


/*
动画类型
*/
public BaseDialog animType(BaseDialog.AnimInType animInType) {


switch (animInType.getIntType()) {
case 0:
getWindow().setWindowAnimations(R.style.dialog_zoom);

break;
case 1:
getWindow().setWindowAnimations(R.style.dialog_anim_left);

break;
case 2:
getWindow().setWindowAnimations(R.style.dialog_anim_top);

break;
case 3:
getWindow().setWindowAnimations(R.style.dialog_anim_right);

break;
case 4:
getWindow().setWindowAnimations(R.style.dialog_anim_bottom);

break;
}
return this;
}


/*
动画类型
*/
public enum AnimInType {
CENTER(0),
LEFT(1),
TOP(2),
RIGHT(3),
BOTTOM(4);

AnimInType(int n) {
intType = n;
}

final int intType;

public int getIntType() {
return intType;
}
}
}

ProgressDialog

public class ProgressDialog extends BaseDialog {

private MaterialProgressDrawable progress;

private ValueAnimator valueAnimator;
private CircleImageView imageView;

public ProgressDialog(Context context) {
super(context);
setCanceledOnTouchOutside(false);

FrameLayout frameLayout = new FrameLayout(context);

imageView = new CircleImageView(context);

progress = new MaterialProgressDrawable(getContext(), imageView);


//设置圈圈的各种大小
progress.updateSizes(MaterialProgressDrawable.DEFAULT);

progress.showArrow(false);
imageView.setImageDrawable(progress);

frameLayout.addView(imageView);


valueAnimator = valueAnimator.ofFloat(0f, 1f);

valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float n = (float) animation.getAnimatedValue();
//圈圈的旋转角度
progress.setProgressRotation(n * 0.5f);
//圈圈周长,0f-1F
progress.setStartEndTrim(0f, n * 0.8f);
//箭头大小,0f-1F
progress.setArrowScale(n);
//透明度,0-255
progress.setAlpha((int) (255 * n));
}
});

getWindow().setLayout(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT);
setContentView(frameLayout);

setOnKeyListener(new OnKeyListener() {
@Override
public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {

if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) {
hide();
return true;
}
return false;
}
});


}


public ProgressDialog duration(long duration) {
valueAnimator.setDuration(duration);

return this;
}


public ProgressDialog radius_iv(float radius_iv) {
imageView.radius(radius_iv);

return this;
}

public ProgressDialog color_iv(int color_iv) {
imageView.color(color_iv);

return this;
}

public ProgressDialog color_bg_progress(int color_bg_progress) {
progress.setBackgroundColor(color_bg_progress);

return this;
}

/**
* //圈圈颜色,可以是多种颜色
*
* @param colors_progress
* @return
*/
public ProgressDialog colors_progress(int... colors_progress) {
progress.setColorSchemeColors(colors_progress);

return this;
}

@Override
public void show() {
super.show();
if (progress == null) return;
progress.start();
if (valueAnimator == null) return;
valueAnimator.start();


}

@Override
public void hide() {
super.hide();
if (progress == null) return;
progress.stop();
if (valueAnimator == null) return;
valueAnimator.cancel();


}
}

参考:Android Dialog

GitHub

APK

收起阅读 »

一个Android文字展示动画框架:TextSurface

文字表面一个小动画框架,可以帮助您以漂亮的方式显示消息。用法创建TextSurface实例或将其添加到您的布局中。创建Text具有TextBuilder定义文本外观和位置的实例:Text textDaai = TextBuilder .create("Daa...
继续阅读 »

文字表面

一个小动画框架,可以帮助您以漂亮的方式显示消息。


用法

  1. 创建TextSurface实例或将其添加到您的布局中。
  2. 创建Text具有TextBuilder定义文本外观和位置的实例

Text textDaai = TextBuilder
.create("Daai")
.setSize(64)
.setAlpha(0)
.setColor(Color.WHITE)
.setPosition(Align.SURFACE_CENTER).build();

  1. 创建动画并将它们传递给TextSurface实例:

textSurface.play(
new Sequential(
Slide.showFrom(Side.TOP, textDaai, 500),
Delay.duration(500),
Alpha.hide(textDaai, 1500)
)
);


调整动画

  • 要按顺序播放动画,请使用 Sequential.java

  • 要同时播放动画,请使用 Parallel.java

  • 动画/效果可以这样组合:

    new Parallel(Alpha.show(textA, 500), ChangeColor.to(textA, 500, Color.RED))

    即文本的 alpha 和颜色将在 500 毫秒内同时更改

添加您自己的动画/效果

您可以扩展两个基本类来添加自定义动画:

Proguard 配置

该框架基于reflection广泛使用的标准 android 动画类为避免混淆,您需要排除框架的类:

-keep class su.levenetc.android.textsurface.** { *; }


下载

repositories {
maven { url "https://jitpack.io" }
}
//...
dependencies {
//...
compile 'com.github.elevenetc:textsurface:0.9.1'
}




github地址:https://github.com/elevenetc/TextSurface
下载地址:master.zip
收起阅读 »

基础篇 - 从构建层面看 import 和 require 的区别

前言 一切的一切,都是因为群里的一个问题 虽然说最近在做 webpack 相关的事情,但是也没有对着干问题做过相关的研究,网上很多文章包括 vue 都介绍了建议使用 import ,但是没有说为什么要使用 import,对于开发者来说,调用的方式是没有区别的...
继续阅读 »

前言


一切的一切,都是因为群里的一个问题


image.png


虽然说最近在做 webpack 相关的事情,但是也没有对着干问题做过相关的研究,网上很多文章包括 vue 都介绍了建议使用 import ,但是没有说为什么要使用 import,对于开发者来说,调用的方式是没有区别的,那么为什么 import 的包就要比 require 的包小呢


这里暂时就不说什么调用方式了,什么动态加载(require)、静态编译(import)的,这个网上都有,这篇文章就是分析一下为什么要用 import,而不用 require


正文


首先本地先基于 webpack 搭建一个环境只是为了测试,不需要搭建太复杂的内容


基础文件内容


// webpack.config.js
module.exports = {
mode: 'development',
entry: './src/index.js'
}

index.js 内添加两种调用方式


function test() {
const { b } = import('./importtest')
console.log(b())
}
test()

// or

function test() {
const { b } = require('./requiretest')
console.log(b())
}
test()

importtest.js 中也是简单输出一下


// importtest.js
export default {
b: function () {
return {
name: 'zhangsan'
}
}
}

requiretest.js 也是如此


// requiretest.js
module.exports = {
b: function() {
return {
name: 'lisi'
}
}
}

上述的方式分别执行 webpack 后,输出的内容分别如下


import 输出


image.png


在打包时一共输出了两个文件:main.jssrc_importtest_js.jsmain.js 里面输出的内容如下


image.png


main.js 里面就是 index.js 里面的内容,importtest 里面的内容,是通过一个索引的方式引用过来的,引用的地址就是 src_importtest_js.js


require 输出


image.png


require 打包时,直接输出了一个文件,就只有一个 main.jsmain.js 里面输出的内容如下


image.png


main.js 里面的内容是 index.jsrequiretest.js 里面的所有内容


综上所述,我们从数据角度来看 import 的包是要大于 require 的,但通过打包文件来看,由业务代码导致的文件大小其实 import 是要小于 require 的
复制代码

多引用情况下导致的打包变化


这个时候我们大概知道了 importrequire 打包的区别,接下来我们可以模拟一下一开始那位同学的问题,直接修改一下 webpack.config.js 的入口即可


module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
index1: './src/index1.js'
}
}
复制代码

这里直接保证 index.jsindex1.js 的内容一样即可,还是先测试一下 import 的打包


image.png


这里的内容和单入口时打包的 import 基本一致,里面出了本身的内容外,都是引用的 src_importtest_js 的地址,那么在看看 require 的包


image.png


这里内容和单入口打包的 require 基本一致,都是把 requiretest 的内容复制到了对应的文件内


虽然我们现在看的感觉多入口打包,还是 import 的文件要比 require 的文件大,但是核心问题在于测试案例的业务代码量比较少,所以看起来感觉 import 要比 require 大,当我们的业务代码量达到实际标准的时候,区别就看出来了


总结


import: 打包的内容是给到一个路径,通过该路径来访问对应的内容


require: 把当前访问资源的内容,打包到当前的文件内


到这里就可以解释为什么 vue 官方和网上的文章说推荐 import 而不推荐 require,因为每一个使用 require 的文件会把当前 require 的内容打包到当前文件内,所以导致了文件的过大,使用 import,抛出来的是一个索引,所以不会导致重复内容的打包,就不会出现包大的情况


当然这也不是绝对的,就好像上述案例那种少量的业务代码,使用 import 的代码量其实要比 require 大,所以不建议大家直接去确定某一种方式是最好的,某一种方式就是不行的,依场景选择方法


尾声


这篇文章就是一个简单的平时技术方面基础研究的简介,不是特别高深的东西,还希望对大家有所帮助,如果有覆盖面不够,或者场景不全面的情况,还希望大家提出,我在继续补充


这种类型的文章不是我擅长的方向,还是喜欢研究一些新的东西,欢迎大家指教:


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

收起阅读 »

小程序页面返回传值四种解决方案总结

使用场景 小程序从A页面跳转到B页面,在B页面选择一个值后返回到A页面,在A页面使用在B页面选中的值。例如:在购买订单页面跳转到地址列表,选择完地址以后回退到订单页面,订单页面的配送地址需要同步更新。 解决方案 常见的比容要容易解决的方案是使用小程序的全局存储...
继续阅读 »

使用场景


小程序从A页面跳转到B页面,在B页面选择一个值后返回到A页面,在A页面使用在B页面选中的值。例如:在购买订单页面跳转到地址列表,选择完地址以后回退到订单页面,订单页面的配送地址需要同步更新。


解决方案


常见的比容要容易解决的方案是使用小程序的全局存储globalData、本地缓存storage、获取小程序的页面栈,调用上一个Page的setData方法、以及利用wx.navigateTo的events属性监听被打开页面发送到当前页面的数据。下面给大家简单对比下四种方法的优缺点:


1、使用globalData实现


//page A
const app = getApp() //获取App.js实例
onShow() {//生命周期函数--监听页面显示
if (app.globalData.backData) {
this.setData({ //将B页面更新完的值渲染到页面上
backData: app.globalData.backData
},()=>{
delete app.globalData.backData //删除数据 避免onShow重复渲染
})
}
}
//page B
const app = getApp() //获取App.js实例
changeBackData(){
app.globalData.backData = '我被修改了'
wx.navigateBack()
}

2、使用本地缓存Storage实现


//page A
onShow: function () {
let backData = wx.getStorageSync('backData')
if(backData){
this.setData({
backData
},()=>{
wx.removeStorageSync('backData')
})
}
},
//page B
changeBackData(){
wx.setStorageSync('backData', '我被修改了')
wx.navigateBack()
},

3、使用小程序的Page页面栈实现


使小程序的页面栈,比其他两种方式会更方便一点而且渲染的会更快一些,不需要等回退到A页面上再把数据渲染出来,在B页面上的直接就会更新A页面上的值,回退到A页面的时候,值已经被更新了。globalData和Storage实现的原理都是在B页面上修改完值以后,回退到A页面,触发onShow生命周期函数,来更新页面渲染。


//page B
changeBackData(){
const pages = getCurrentPages();
const beforePage = pages[pages.length - 2]
beforePage.setData({ //会直接更新A页面的数据,A页面不需要其他操作
backData: "我被修改了"
})

4、使用wx.navigateTo API的events实现


wx.navigateTo的events的实现原理是利用设计模式的发布订阅模式实现的,有兴趣的同学可以自己动手实现一个简单的,也可以实现相同的效果。


//page A
goPageB() {
wx.navigateTo({
url: 'B',
events: {
getBackData: res => { //在events里面添加监听事件
this.setData({
backData: res.backData
})
},
},
})
},
//page B
changeBackData(){
const eventChannel = this.getOpenerEventChannel()
eventChannel.emit('getBackData', {
backData: '我被修改了'
});
wx.navigateBack()
}

总结


1和2两种方法在页面渲染效果上比后面两种稍微慢一点,3和4两种方法在B页面回退到A页面之前已经触发了更新,而1和2两种方法是等返回到A页面之后,在A页面才触发更新。并且1和2两种方式,要考虑到A页面更新完以后要删除globalData和Storage的数据,避免onShow方法里面重复触发setData更新页面,所以个人更推荐大家使用后面的3和4两种方式。


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

收起阅读 »

腾讯面试官:兄弟,你说你会Webpack,那说说他的原理?

原理图解 1、首先肯定是要先解析入口文件entry,将其转为AST(抽象语法书),使用@babel/parser 2、然后使用@babel/traverse去找出入口文件所有依赖模块 3、然后使用@babel/core+@babel/preset-env将入...
继续阅读 »

image.png


原理图解



  • 1、首先肯定是要先解析入口文件entry,将其转为AST(抽象语法书),使用@babel/parser

  • 2、然后使用@babel/traverse去找出入口文件所有依赖模块

  • 3、然后使用@babel/core+@babel/preset-env将入口文件的AST转为Code

  • 4、将2中找到的入口文件的依赖模块,进行遍历递归,重复执行1,2,3

  • 5。重写require函数,并与4中生成的递归关系图一起,输出到bundle


截屏2021-07-21 上午7.39.26.png


代码实现


webpack具体实现原理是很复杂的,这里只是简单实现一下,让大家粗略了解一下,webpack是怎么运作的。在代码实现过程中,大家可以自己console.log一下,看看ast,dependcies,code这些具体长什么样,我这里就不展示了,自己去看会比较有成就感,嘿嘿!!


image.png


目录


截屏2021-07-21 上午7.47.33.png


config.js


这个文件中模拟webpack的配置


const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'main.js'
}
}

入口文件


src/index.js是入口文件


// src/index
import { age } from './aa.js'
import { name } from './hh.js'

console.log(`${name}今年${age}岁了`)

// src/aa.js
export const age = 18

// src/hh.js
console.log('我来了')
export const name = '林三心'


1. 定义Compiler类


// index.js
class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模块
this.modules = []
}
// 构建启动
run() {}
// 重写 require函数,输出bundle
generate() {}
}

2. 解析入口文件,获取 AST


我们这里使用@babel/parser,这是babel7的工具,来帮助我们分析内部的语法,包括 es6,返回一个 AST 抽象语法树


const fs = require('fs')
const parser = require('@babel/parser')
const options = require('./webpack.config')

const Parser = {
getAst: path => {
// 读取入口文件
const content = fs.readFileSync(path, 'utf-8')
// 将文件内容转为AST抽象语法树
return parser.parse(content, {
sourceType: 'module'
})
}
}

class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模块
this.modules = []
}
// 构建启动
run() {
const ast = Parser.getAst(this.entry)
}
// 重写 require函数,输出bundle
generate() {}
}

new Compiler(options).run()

3. 找出所有依赖模块


Babel 提供了@babel/traverse(遍历)方法维护这 AST 树的整体状态,我们这里使用它来帮我们找出依赖模块


const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default

const Parser = {
getAst: path => {
// 读取入口文件
const content = fs.readFileSync(path, 'utf-8')
// 将文件内容转为AST抽象语法树
return parser.parse(content, {
sourceType: 'module'
})
},
getDependecies: (ast, filename) => {
const dependecies = {}
// 遍历所有的 import 模块,存入dependecies
traverse(ast, {
// 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
// 保存依赖模块路径,之后生成依赖关系图需要用到
const filepath = './' + path.join(dirname, node.source.value)
dependecies[node.source.value] = filepath
}
})
return dependecies
}
}

class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模块
this.modules = []
}
// 构建启动
run() {
const { getAst, getDependecies } = Parser
const ast = getAst(this.entry)
const dependecies = getDependecies(ast, this.entry)
}
// 重写 require函数,输出bundle
generate() {}
}

new Compiler(options).run()

4. AST 转换为 code


AST 语法树转换为浏览器可执行代码,我们这里使用@babel/core 和 @babel/preset-env


const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')

const Parser = {
getAst: path => {
// 读取入口文件
const content = fs.readFileSync(path, 'utf-8')
// 将文件内容转为AST抽象语法树
return parser.parse(content, {
sourceType: 'module'
})
},
getDependecies: (ast, filename) => {
const dependecies = {}
// 遍历所有的 import 模块,存入dependecies
traverse(ast, {
// 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
// 保存依赖模块路径,之后生成依赖关系图需要用到
const filepath = './' + path.join(dirname, node.source.value)
dependecies[node.source.value] = filepath
}
})
return dependecies
},
getCode: ast => {
// AST转换为code
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
return code
}
}

class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模块
this.modules = []
}
// 构建启动
run() {
const { getAst, getDependecies, getCode } = Parser
const ast = getAst(this.entry)
const dependecies = getDependecies(ast, this.entry)
const code = getCode(ast)
}
// 重写 require函数,输出bundle
generate() {}
}

new Compiler(options).run()

5. 递归解析所有依赖项,生成依赖关系图


const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')

const Parser = {
getAst: path => {
// 读取入口文件
const content = fs.readFileSync(path, 'utf-8')
// 将文件内容转为AST抽象语法树
return parser.parse(content, {
sourceType: 'module'
})
},
getDependecies: (ast, filename) => {
const dependecies = {}
// 遍历所有的 import 模块,存入dependecies
traverse(ast, {
// 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
// 保存依赖模块路径,之后生成依赖关系图需要用到
const filepath = './' + path.join(dirname, node.source.value)
dependecies[node.source.value] = filepath
}
})
return dependecies
},
getCode: ast => {
// AST转换为code
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
return code
}
}

class Compiler {
constructor(options) {
// webpack 配