zoukankan      html  css  js  c++  java
  • 调度器25—进程冻结 Hello

    基于linux-5.10

    一、任务冻结概述

    进程冻结是当系统hibernate或者suspend时,对进程进行暂停挂起的一种机制,本质上是对先将任务状态设置为 TASK_UNINTERRUPTIBLE,然后再调用schedule()将任务切走。主要用于配合系统的suspend和resume相关机制,当然freezer cgroup也提供了对一批进程进行冻结的机制。用户空间进程默认可以被冻结,内核线程默认不能被动冻结。

    1. 有3个 per-task 的flag用于描述进程冻结状态:

    PF_NOFREEZE:表示此任务是否允许被冻结,1表示不可冻结,0表示可冻结
    PF_FROZEN:表示进程是否已经处于被冻结状态
    PF_FREEZER_SKIP:冻结时跳过此任务,freeze_task()和系统休眠流程中的冻结判断有此标志位的任务就会跳过它,使用 freezer_should_skip()来判断此标志位

    2. 有3个相关的全局变量:

    system_freezing_cnt:大于0表示系统进入了冻结状态
    pm_freezing: true表示用户进程被冻结
    pm_nosig_freezing: true表示内核进程和workqueue被冻结

    赋值调用路径:

    state_store //用户echo mem > state
        pm_suspend //suspend.c 触发系统休眠的入口函数,autosleep.c hiberation.c中也有调用
            enter_state //suspend.c
                suspend_prepare //suspend.c 其它位置也有调用,系统挂起流程中最先执行 ######
                    suspend_freeze_processes power.h
                        freeze_processes //process.c
                            atomic_inc(&system_freezing_cnt);
                            pr_info("Freezing user space processes ... ");
                            pm_freezing = true;
                        freeze_kernel_threads
                            pr_info("Freezing remaining freezable tasks ... ");
                            pm_nosig_freezing = true;
                ------唤醒------
                suspend_finish //suspend.c 系统唤醒流程中最后执行 #######
                    suspend_thaw_processes
                        thaw_processes
                            atomic_dec(&system_freezing_cnt);
                                pm_freezing = false;
                                pm_nosig_freezing = false;
                                pr_info("Restarting tasks ... ");

    报错后回退的执行路径,非主要路径:

    suspend_prepare //suspend.c
        suspend_freeze_processes //power.h
            freeze_kernel_threads //只有执行try_to_freeze_tasks()返回错误时才执行
                thaw_kernel_threads
                    pm_nosig_freezing = false;
                    pr_info("Restarting kernel threads ... ");

    注:由于没有使能 CONFIG_HIBERNATION 和 CONFIG_HIBERNATION_SNAPSHOT_DEV,因此 kernel/power/user.c 和 kernel/power/hibernate.c 是不使用的。

    3. 冻结和解冻的主要函数:

    freeze_processes():
       - 仅冻结用户空间任务
      
    freeze_kernel_threads():
       - 冻结所有任务(包括内核线程),因为无法在不冻结用户空间任务的情况下冻结内核线程
    
    thaw_kernel_threads():
       - 仅解冻内核线程;如果需要在内核线程的解冻和用户空间任务的解冻之间做一些特殊的事情,或者如果想推迟用户空间任务的解冻,这将特别有用
    
    thaw_processes():
       - 解冻所有任务(包括内核线程),因为无法在不解冻内核线程的情况下解冻用户空间任务

    4. 需要冻结的原因

    (1) 防止文件系统在休眠后被损坏。目前我们没有简单的检查点文件系统的方法,所以如果休眠流程执行后对磁盘上的文件系统数据和/或元数据进行了任何修改,我们无法将它们恢复到修改之前的状态。
    (2) 防止为创建休眠镜像唤出内存后进程重新分配内存。
    (3) 防止用户空间进程和一些内核线程干扰设备的挂起和恢复。尽管如此,还是有一些内核线程想要被冻结,比如驱动的内核线程原则上需要知道设备合适挂起以便不再访问它们。若其内核线程是可冻结的,就可以做到在其.suspend()回调之前冻结,并在其.resume() 回调之后解冻。
    (4) 防止用户空间进程意识到发生了休眠(或挂起)操作。

    二、冻结实现机制

    1. 冻结核心函数之 __refrigerator()

    //kernel/freezer.c
    bool __refrigerator(bool check_kthr_stop)
    {
        /* Hmm, should we be allowed to suspend when there are realtime processes around? */
        bool was_frozen = false;
        long save = current->state;
    
        pr_debug("%s entered refrigerator\n", current->comm);
    
        for (;;) {
            set_current_state(TASK_UNINTERRUPTIBLE);
    
            spin_lock_irq(&freezer_lock);
            current->flags |= PF_FROZEN; //标记任务被冻结
            //若是不运行冻结当前任务,或在要求检查内核线程should_stop且是should_stop时,取消冻结
            if (!freezing(current) || (check_kthr_stop && kthread_should_stop()))
                current->flags &= ~PF_FROZEN; //取消任务被冻结标记
            trace_android_rvh_refrigerator(pm_nosig_freezing);
            spin_unlock_irq(&freezer_lock);
    
            //若是当前线程不允许被冻结,就退出
            if (!(current->flags & PF_FROZEN))
                break;
            was_frozen = true;
            //将当前任务切走,resume后从这里继续开始执行
            schedule();
        }
    
        pr_debug("%s left refrigerator\n", current->comm);
    
        /*
         * Restore saved task state before returning.  The mb'd version
         * needs to be used; otherwise, it might silently break
         * synchronization which depends on ordered task state change.
         */
        set_current_state(save); //current->state=save 
    
        //返回是否被冻结的状态
        return was_frozen;
    }
    EXPORT_SYMBOL(__refrigerator);
    
    
    //include/linux/freezer.h 检查是否允许冻结此任务
    static inline bool freezing(struct task_struct *p)
    {
        //若是系统没有处于冻结流程中,直接不允许冻结
        if (likely(!atomic_read(&system_freezing_cnt)))
            return false;
        return freezing_slow_path(p);
    }
    
    
    //kernel/freezer.c 检测一个任务是否应该被冻结的慢速路径
    bool freezing_slow_path(struct task_struct *p)
    {
        /*
         * 若此线程不运行被冻结 或 是执行freeze_processes()
         * 的那个任务,就不应该被冻结,毕竟不应该冻结自己,
         * 否则怎么继续执行suspend流程呢。
         */
        if (p->flags & (PF_NOFREEZE | PF_SUSPEND_TASK))
            return false;
    
        //被 OOM killer 干掉的任务不应该被冻结
        if (test_tsk_thread_flag(p, TIF_MEMDIE))
            return false;
    
        /*
         * 系统suspend流程已经走到冻结内核线程了或是任务所在的
         * cgroup进行的冻结,那允许冻结.
         */
        if (pm_nosig_freezing || cgroup_freezing(p))
            return true;
    
        /*
         * 系统suspend流程已经走到冻结用户空间任务了但是还没有
         * 走到冻结内核线程那里,若非内核线程,也就是用户空间进
         * 程,就允许冻结
         */
        if (pm_freezing && !(p->flags & PF_KTHREAD))
            return true;
    
        //否则不运行冻结
        return false;
    }
    EXPORT_SYMBOL(freezing_slow_path);

    可以看到,对任务进行冻结的本质就是将任务的状态设置为 TASK_UNINTERRUPTIBLE,然后将任务切走。但注意这里判断的只是current线程,使用上有限制。

    2. 任务冻结

    /*
     * kernel/freezer.c
     *
     * freeze_task - 向给定任务发送冻结请求
     * @p: 向此任务发送请求
     *
     * 如果@p 正在冻结,则通过发送假信号(如果它不是内核线程)或唤醒它(如果它是内核线程)来发送冻结请求。
     *
     * 返回:%false,如果@p 没有冻结或已经冻结; 否则返回%true
     */
    bool freeze_task(struct task_struct *p)
    {
        unsigned long flags;
    
        /*
         * This check can race with freezer_do_not_count, but worst case that
         * will result in an extra wakeup being sent to the task.  It does not
         * race with freezer_count(), the barriers in freezer_count() and
         * freezer_should_skip() ensure that either freezer_count() sees
         * freezing == true in try_to_freeze() and freezes, or
         * freezer_should_skip() sees !PF_FREEZE_SKIP and freezes the task
         * normally.
         */
        //跳过标记为 PF_FREEZER_SKIP 的任务
        if (freezer_should_skip(p))
            return false;
    
        spin_lock_irqsave(&freezer_lock, flags);
        //如果不允许冻结或已经被冻结,则返回false
        if (!freezing(p) || frozen(p)) {
            spin_unlock_irqrestore(&freezer_lock, flags);
            return false;
        }
    
        //用户进程和内核线程的冻结机制不同
        if (!(p->flags & PF_KTHREAD))
            fake_signal_wake_up(p); //通过一个假信号唤醒用户进程,注意也是只唤醒 INTERRUPTIBLE 类型的用户进程
        else
            wake_up_state(p, TASK_INTERRUPTIBLE); //唤醒内核线程,注意只唤醒 INTERRUPTIBLE 类型的内核线程
    
        spin_unlock_irqrestore(&freezer_lock, flags);
        return true;
    }

    (1) 冻结用户进程

    //kernel/freezer.c
    static void fake_signal_wake_up(struct task_struct *p)
    {
        unsigned long flags;
    
        if (lock_task_sighand(p, &flags)) {
            signal_wake_up(p, 0); //通过信号唤醒任务
            unlock_task_sighand(p, &flags);
        }
    }
    
    static inline void signal_wake_up(struct task_struct *t, bool resume)
    {
        signal_wake_up_state(t, resume ? TASK_WAKEKILL : 0); //上面传参是0,这里也是0
    }
    
    void signal_wake_up_state(struct task_struct *t, unsigned int state)
    {
        //设置 TIF_SIGPENDING=bit0, check的时候位与 _TIF_SIGPENDING=(1<<0)
        set_tsk_thread_flag(t, TIF_SIGPENDING);
    
        if (!wake_up_state(t, state | TASK_INTERRUPTIBLE)) //state传的是0,也是只唤醒INTERRUPTIBLE类型的任务
            kick_process(t);
    }

    接着会去走任务唤醒流程,由于休眠是发生在内核空间,最终肯定会调用 ret_to_user 来返回用户空间。

    ret_to_user //arm64/kernel/entry.S
        work_pending
            do_notify_resume //arm64/kernel/signal.c
                do_signal(regs) //if(thread_flags & _TIF_SIGPENDING) 为真调用
                    get_signal(&ksig)
                        try_to_freeze //include/linux/freeze.h

    try_to_freeze 函数:

    static inline bool try_to_freeze(void)
    {
        if (!(current->flags & PF_NOFREEZE))
            debug_check_no_locks_held();
        return try_to_freeze_unsafe();
    }
    
    static inline bool try_to_freeze_unsafe(void)
    {
        //指示当前函数可能睡眠
        might_sleep();
        //判断当前进程是否需要冻结
        if (likely(!freezing(current)))
            return false;
        //进行实际的冻结
        return __refrigerator(false);
    }

    (2) 冻结内核线程

    对内核线程的冻结,主要是唤醒 TASK_INTERRUPTIBLE 状态的内核线程,然后由内核线程自己进行冻结自己的操作。例如 freezing-of-tasks.rst 中举的一个例子:

    //例1:
    set_freezable();
    do {
        hub_events();
        wait_event_freezable(khubd_wait, !list_empty(&hub_event_list) || kthread_should_stop());
    } while (!kthread_should_stop() || !list_empty(&hub_event_list));
    
    //例2:
    static int tps65090_charger_poll_task(void *data)
    {
        set_freezable();
    
        while (!kthread_should_stop()) {
            schedule_timeout_interruptible(POLL_INTERVAL);
            try_to_freeze();
            tps65090_charger_isr(-1, data);
        }
        return 0;
    }

    由于内核线程默认是不可被冻结的,因此希望自己被冻结的内核线程需要先调用 set_freezable() 即 current->flags &= ~PF_NOFREEZE 清除PF_NOFREEZE标志位,将自己设置为可冻结的。然后在轮询逻辑中调用 try_to_freeze() 以便在系统suspend流程中唤醒内核线程时能自己冻结自己。

    3. 系统suspend时全局冻结

    在系统suspend早期,suspend_prepare()时调用 suspend_freeze_processes() 进行任务冻结

    static inline int suspend_freeze_processes(void)
    {
        int error;
    
        error = freeze_processes(); //冻结用户进程
        /*
         * freeze_processes() automatically thaws every task if freezing
         * fails. So we need not do anything extra upon error.
         */
        if (error)
            return error;
    
        error = freeze_kernel_threads(); //冻结内核线程
        /*
         * freeze_kernel_threads() thaws only kernel threads upon freezing
         * failure. So we have to thaw the userspace tasks ourselves.
         */
        if (error)
            thaw_processes();
    
        return error;
    }

    冻结所有的用户进程:

    /*
     * freeze_processes - 向用户空间进程发出信号以进入冻结。当前线程不会被冻结。
     * 调用 freeze_processes() 的这个进程必须在之后调用 thaw_processes()。
     *
     * 成功时,返回 0。失败时,-errno 和系统完全解冻。
     */
    int freeze_processes(void)
    {
        int error;
    
        //固件加载时有使用到这一机制
        error = __usermodehelper_disable(UMH_FREEZING);
        if (error)
            return error;
    
        /* Make sure this task doesn't get frozen */
        current->flags |= PF_SUSPEND_TASK; //在freezing_slow_path()中会判断
    
        if (!pm_freezing)
            atomic_inc(&system_freezing_cnt); //标记开始冻结用户空间进程
    
        pm_wakeup_clear(true);
        pr_info("Freezing user space processes ... ");
        pm_freezing = true;
        error = try_to_freeze_tasks(true);
        if (!error) {
            __usermodehelper_set_disable_depth(UMH_DISABLED); //冻结失败后的恢复操作
            pr_cont("done.");
        }
        pr_cont("\n");
        BUG_ON(in_atomic());
    
        /*
         * Now that the whole userspace is frozen we need to disable
         * the OOM killer to disallow any further interference with
         * killable tasks. There is no guarantee oom victims will
         * ever reach a point they go away we have to wait with a timeout.
         */
        if (!error && !oom_killer_disable(msecs_to_jiffies(freeze_timeout_msecs)))
            error = -EBUSY;
    
        if (error)
            thaw_processes(); //冻结失败后的恢复操作
        return error;
    }
    
    
    //参数user_only为假就只冻结用户空间进程,若是为真内核线程也冻结。
    static int try_to_freeze_tasks(bool user_only)
    {
        struct task_struct *g, *p;
        unsigned long end_time;
        unsigned int todo;
        bool wq_busy = false;
        ktime_t start, end, elapsed;
        unsigned int elapsed_msecs;
        bool wakeup = false;
        int sleep_usecs = USEC_PER_MSEC;
    
        start = ktime_get_boottime();
    
        /*
         * 允许冻结所有用户空间进程或所有可冻结内核线程最多花费多长时间,來自 /sys/power/pm_freeze_timeout,
         * 单位毫秒,默认值为 20000ms.
         */
        end_time = jiffies + msecs_to_jiffies(freeze_timeout_msecs);
    
        if (!user_only)
            freeze_workqueues_begin(); //开始冻结内核工作队列
    
        //死循环去轮询,直到没有需要被冻结的任务了,或超时了,或有唤醒事件触发了。
        while (true) {
            todo = 0; //每一轮都是从0开始 ######
            read_lock(&tasklist_lock); //需要持有这个锁进行遍历所有任务
    
            //对每一个任务都执行
            for_each_process_thread(g, p) {
                //freeze_task中进行实际的冻结,对于用户进程发信号,对于内核线程是唤醒
                if (p == current || !freeze_task(p))
                    continue;
    
                //跳过标记了 PF_FREEZER_SKIP 标记的任务
                if (!freezer_should_skip(p))
                    todo++; //统计的需要冻结的线程数量
            }
            read_unlock(&tasklist_lock);
    
            if (!user_only) {
                //只要有一个 pool_workqueue::nr_active 不为0,就返回true
                wq_busy = freeze_workqueues_busy();
                todo += wq_busy;
            }
    
            //若没有需要冻结的任务了,或超时了,就退出
            if (!todo || time_after(jiffies, end_time))
                break;
    
            //有pending的唤醒事件,就要退出系统的休眠流程
            if (pm_wakeup_pending()) {
                wakeup = true;
                break;
            }
    
            /*
             * We need to retry, but first give the freezing tasks some
             * time to enter the refrigerator.  Start with an initial
             * 1 ms sleep followed by exponential backoff until 8 ms.
             */
            //睡眠 0.5ms-- 8ms, 避免轮询的太频繁导致高负载
            usleep_range(sleep_usecs / 2, sleep_usecs);
            if (sleep_usecs < 8 * USEC_PER_MSEC)
                sleep_usecs *= 2;
        }
    
        //使用单调增的boottime时钟记录上面轮询持续的时间
        end = ktime_get_boottime();
        elapsed = ktime_sub(end, start);
        elapsed_msecs = ktime_to_ms(elapsed);
    
        if (wakeup) { //由唤醒事件导致的轮询退出
            pr_cont("\n");
            pr_err("Freezing of tasks aborted after %d.%03d seconds", elapsed_msecs/1000, elapsed_msecs%1000);
        } else if (todo) { //由超时导致的轮询退出
            pr_cont("\n");
            pr_err("Freezing of tasks failed after %d.%03d seconds (%d tasks refusing to freeze, wq_busy=%d):\n",
                   elapsed_msecs/1000, elapsed_msecs%1000, todo-wq_busy, wq_busy);
    
            //若是在没能冻结的任务中有workqueue,还会打印出workqueue的状态
            if (wq_busy)
                show_workqueue_state();
    
            /*
             * 若是使能了 CONFIG_PM_SLEEP_DEBUG,可由/sys/power/pm_debug_messages来控制,否则只能通过"pm_debug_messages"
             * 这个启动参数来使能这个debug开关。也可以通过改代码的形式默认设为true,无其它依赖。
             */
            if (pm_debug_messages_on) {
                read_lock(&tasklist_lock);
                for_each_process_thread(g, p) {
                    /*
                     * 遍历系统中的每一个任务,若此任务不是正在执行冻结的任务,又不是冻结需要跳过的任务,又是需要被冻结的
                     * 任务,但是又没有被冻结,则会打印这类任务的信息。其中信息包括:任务名、任务状态、父任务、任务此时flags、
                     * 若是workqueue中的worker线程还会打印workqueue信息、栈回溯。
                     */
                    if (p != current && !freezer_should_skip(p) && freezing(p) && !frozen(p)) {
                        sched_show_task(p);
                        trace_android_vh_try_to_freeze_todo_unfrozen(p);
                    }
                }
                read_unlock(&tasklist_lock);
            }
    
            trace_android_vh_try_to_freeze_todo(todo, elapsed_msecs, wq_busy);
        } else { //需要冻结的任务都冻结成功了
            //打印冻结持续的时间
            pr_cont("(elapsed %d.%03d seconds) ", elapsed_msecs/1000, elapsed_msecs%1000);
        }
    
        //若返回非0,则整个休眠流程会终止。
        return todo ? -EBUSY : 0;
    }

    冻结所有内核线程:

    int freeze_kernel_threads(void)
    {
        int error;
    
        pr_info("Freezing remaining freezable tasks ... ");
    
        pm_nosig_freezing = true;
        error = try_to_freeze_tasks(false);
        if (!error)
            pr_cont("done.");
    
        pr_cont("\n");
        BUG_ON(in_atomic());
    
        if (error)
            thaw_kernel_threads();
        return error;
    }

    和冻结所有用户进程的 freeze_processes() 相比较可以发现,主要是 try_to_freeze_tasks() 传的参数不同,而参数导致的差异也只是是否冻结workqueue。

    三、任务解冻

    在系统唤醒的后期,会执行 suspend_thaw_processes() 来唤醒所有任务

    static inline void suspend_thaw_processes(void)
    {
        thaw_processes();
    }
    
    void thaw_processes(void)
    {
        struct task_struct *g, *p;
        struct task_struct *curr = current;
    
        trace_suspend_resume(TPS("thaw_processes"), 0, true);
        if (pm_freezing)
            atomic_dec(&system_freezing_cnt); //计算减1,表示退出系统suspend
        pm_freezing = false;
        pm_nosig_freezing = false;
    
        oom_killer_enable();
    
        pr_info("Restarting tasks ... ");
    
        __usermodehelper_set_disable_depth(UMH_FREEZING);
        thaw_workqueues(); //解冻workqueue
    
        cpuset_wait_for_hotplug();
    
        read_lock(&tasklist_lock);
    
        //对系统中的每个任务都执行
        for_each_process_thread(g, p) {
            /* No other threads should have PF_SUSPEND_TASK set */
            //只有当前执行冻结/解冻的线程才能有 PF_SUSPEND_TASK 标志
            WARN_ON((p != curr) && (p->flags & PF_SUSPEND_TASK));
            __thaw_task(p);
        }
        read_unlock(&tasklist_lock);
    
        WARN_ON(!(curr->flags & PF_SUSPEND_TASK));
        curr->flags &= ~PF_SUSPEND_TASK; //执行完解冻清除当前任务的 PF_SUSPEND_TASK 标志。
        usermodehelper_enable();
    
        schedule(); //将当前线程切走,当前线程就是echo mem > /sys/power/suspend的线程,即spspend hal线程
        pr_cont("done.\n");
        trace_suspend_resume(TPS("thaw_processes"), 0, false);
    }

    唤醒不再区分是内核线程还是用户空间进程,统一执行 __thaw_task(p),而这个函数只是简单的执行 wake_up_process(p),执行唤醒任务的动作。

    void __thaw_task(struct task_struct *p)
    {
        unsigned long flags;
        const struct cpumask *mask = task_cpu_possible_mask(p);
    
        spin_lock_irqsave(&freezer_lock, flags);
        /*
         * Wake up frozen tasks. On asymmetric systems where tasks cannot
         * run on all CPUs, ttwu() may have deferred a wakeup generated
         * before thaw_secondary_cpus() had completed so we generate
         * additional wakeups here for tasks in the PF_FREEZER_SKIP state.
         */
        //若任务p是被冻结状态的,或是冻结或跳过冻结的且其亲和性不是所有的possible cpu,就唤醒任务P
        if (frozen(p) || (frozen_or_skipped(p) && mask != cpu_possible_mask))
            /*
             * 注意这里唤醒的是 TASK_NORMAL    也即是 INTERRUPTIBLE 和 UNINTERRUPTIBLE 的任务,
             * 而冻结时唤醒的只是 INTERRUPTIBLE 类型的任务。
             */
            wake_up_process(p);
        spin_unlock_irqrestore(&freezer_lock, flags);
    }

    既然冻结和解冻都是唤醒线程,区别是什么呢。区别就是 system_freezing_cnt、pm_freezing、pm_nosig_freezing 三个变量的值不同,在系统suspend时的冻结流程中,它们为true,在 freezing() 函数中判断为需要冻结,而在系统resume时的解冻流程中 freezing() 函数中判断为不需要冻结,则会进行解冻。

    四、总结

    1. 冻结的本质就是先将任务设置为 UNINTERRUPTABLE 状态,然后再将任务切走。

    2. 冻结和解冻都是靠唤醒任务实现的,根据 system_freezing_cnt、pm_freezing、pm_nosig_freezing 三个变量的值不同来决定是冻结还是解冻。

    3. 用户进程默认是可冻结的,系统suspend流程中会自动冻结用户进程。而内核进程默认是不可以被冻结的,若是冻结指定的内核线程需要内核线程自己先清除自己的 PF_NOFREEZE 标志位,然后调用 try_to_freeze()函数冻结自己。系统冻结框架做的仅仅是唤醒内核线程而已。

    4. 用户进程的冻结借助假信号来完成,只是设置 TIF_SIGPENDING 标志位置,在唤醒进程返回用户空间的过程中发现在系统系统在冻结用户进程,就会调用 try_to_freeze()函数冻结自己。

    5. 驱动的内核线程,在设备驱动的.suspend 回调调用之后不再支持访问的,需要将其内核线程接入到冻结/解冻机制中。

    6. freeze cgroup 对用户空间提供了冻结/解冻进程的机制,可以根据自己系统的特性进行优化拓展。

    參考:
    Documentation/power/freezing-of-tasks.rst 翻译

    Cgroup内核文档翻译(5)——Documentation/cgroup-v1/freezer-subsystem.txt

  • 相关阅读:
    iOS越狱系列(一):使用Reveal分析APP
    ios-异步消息同步问题-典型使用场景: 微信私信界面
    ios 消息跳转处理
    iOS开发UI篇—IOS CoreText.framework --- 基本用法
    IOS开发之实现App消息推送(最新)
    Thread 1: signal SIGABRT-内存管理的陋习
    别用symbolicatecrash来解析crash Log了by 风之枫
    通过崩溃trace来查找问题原因 .
    Xcode 6视图调试小贴士
    调试message send to deallocated instance问题
  • 原文地址:https://www.cnblogs.com/hellokitty2/p/15785973.html
Copyright © 2011-2022 走看看