0%

Objective-C 基础

NSString / NSArray 为什么用 copy 不用 strong

  • @property 声明 NSString、NSArray、NSDictionary 经常使用 copy 关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary,他们之间可能进行赋值操作(就是把可变的赋值给不可变的),为确保对象中的字符串值不会无意间变动,应该在设置新属性值时拷贝一份。
  • 因为父类指针可以指向子类对象,使用 copy 的目的是为了让本对象的属性不受外界影响,使用 copy 无论给我传入是一个可变对象还是不可对象,我本身持有的就是一个不可变的副本。
  • 如果我们使用是 strong ,那么这个属性就有可能指向一个可变对象,如果这个可变对象在外部被修改了,那么会影响该属性。
  • 总结:使用copy的目的是,防止把可变类型的对象赋值给不可变类型的对象时,可变类型对象的值发送变化会无意间篡改不可变类型对象原来的值。

OC 为什么是动态运行时语言

  • 主要是将数据类型的确定由编译时,推迟到了运行时。简单来说, 运行时机制使我们直到运行时才去决定一个对象的类别,以及调用该类别对象指定方法。

IBOutlet连出来的视图属性为什么可以被设置成weak

  • 使用storyboard创建的ViewController,那么会有一个叫 _topLevelObjectsToKeepAliveFromStoryboard 的私有数组强引用所有top level的对象,同时top level对象强引用所有子对象,那么vc没必要再强引用top level对象的子对象。

@property 的本质是什么?

  • @property = ivar + getter + setter;

怎么使用 copy 关键字

  • NSStringNSArrayNSDictionary 等等经常使用copy关键字,是因为他们有对应的可变类型:NSMutableStringNSMutableArrayNSMutableDictionary
  • block 也经常使用 copy 关键字,具体原因见官方文档:Objects Use Properties to Keep Track of Blocks:
  • block 使用 copy 是从 MRC 遗留下来的“传统”,在 MRC 中,方法内部的 block 是在栈区的,使用 copy 可以把它放到堆区.
  • 在 ARC 中写不写都行:
  • 在 ARC 环境下,编译器会根据情況自动将栈上的 block 复制到堆上,比如以下情况:

    block 作为函数返回值时
    将 block 赋值给 __strong 指针时(property 的 copy 属性对应的是这一条)
    block 作为 Cocoa API 中方法名含有 using Block 的方法参数时
    block 作为 GCD API 的方法参数时
    其中, block 的 property 设置为 copy, 对应的是这一条:将 block 赋值给 __strong 指针时。
    换句话说:
    对于 block 使用 copy 还是 strong 效果是一样的,但写上 copy 也无伤大雅,还能时刻提醒我们:编译器自动对 block 进行了 copy 操作。如果不写 copy ,该类的调用者有可能会忘记或者根本不知道“编译器会自动对 block 进行了 copy 操作”,他们有可能会在调用之前自行拷贝属性值。这种操作多余而低效。你也许会感觉我这种做法有些怪异,不需要写还依然写。如果你这样想,其实是你“日用而不知”,你平时开发中是经常在用我说的这种做法的,比如下面的属性不写copy也行,但是你会选择写还是不写呢?

最近接触了 Kubernetes 的部署,最早看的是基于 ansible 的部署方案 https://github.com/easzlab/kubeasz,后经 leader 提示改看 rancher,发现 rancher 更加方便,遂作此文以记录。同时,此项目也值得一看 https://github.com/opsnull/follow-me-install-kubernetes-cluster

常用命令

  • 查看 Kubernetes 状态

kubectl version

正常应该显示

1
2
Client Version: version.Info{Major:"1", Minor:"14", GitVersion:"v1.14.3", GitCommit:"5e53fd6bc17c0dec8434817e69b04a25d8ae0ff0", GitTreeState:"clean", BuildDate:"2019-06-07T09:55:27Z", GoVersion:"go1.12.5", Compiler:"gc", Platform:"darwin/amd64"}
Server Version: version.Info{Major:"1", Minor:"13", GitVersion:"v1.13.5", GitCommit:"2166946f41b36dea2c4626f90a77706f426cdea2", GitTreeState:"clean", BuildDate:"2019-03-25T15:19:22Z", GoVersion:"go1.11.5", Compiler:"gc", Platform:"linux/amd64"}
  • 查看所有容器的状态

kubectl get pods --all-namespaces

  • 一次性测试工具,可以用于模拟基础容器内的环境,用完会自动删除

kubectl run --generator=run-pod/v1 test --rm -it --image=alpine /bin/sh

发布服务的注意点

硬盘

  • vSphere 下支持自动创建硬盘,但不支持扩大已经指定好的硬盘大小。因此指定最大硬盘大小时,可以尽可能给大。因为不会全部用完,是按需使用的。实测如果给 10Ti,那么初始会用掉 1Ti,之后缓慢增加

  • 需要在部署 Kubernetes 后,手工创建 StorageClass,具体命令参见 [部署步骤] - [vSphere] 一节

  • 发布需要存储的 docker 服务后,如果想删除,需要手工去 vSphere 的硬盘里自行删除自动创建的硬盘。这是因为 StorageClass 的 reclaimPolicy 被设置成了 Retain。这是为了避免移除服务时数据被自动清空

  • PersistentVolumeClaim 指定的 StorageClass 应为 eudicssd 或者 eudichdd,具体需要看情况按需使用

  • StatefulSet 如果设置了 replicas > 1,且配置了volumeClaimTemplates,那么新建的硬盘内是没有任何数据的。如果只是配置了 volumeClaim,那么 replicas > 1 时,第二个 Pod 创建会永远失败,因为目前 vSphere 的硬盘策略是只允许 ReadWriteOnce

亲和性调度

  • 对于硬件要求,可用性要求较高的服务,需要配置亲和性调度。避免 Kuberneters 将服务部署到硬件条件不足的 Node 上

  • Node 不可用后,会在大约5分钟后才调度到新的 Node 上,具体参见 https://kubernetes.io/zh/docs/concepts/architecture/nodes/

gRPC

  • 只能通过 NodePort 部署,不支持 Ingress,但由于 NodePort 的特性是会在每个 worker 上开同样的端口,因此问题不大,可以通过之前在 pfsense 里绑定的域名 test.local 直接访问

  • NodePort 端口范围: 30000-32767,可以在刚刚的 cloud-config.yml 里修改范围,但最好不要太低


部署

部署机器的最佳实践

  • 1 台运行 rancher 的机器,这台机器里不要部署 Kuberneters,因为 rancher 在部署 Kuberneters 时会新建许多 docker container,导致混乱

  • 3 台 etcd,其中硬盘最好给ssd,内存尽可能给多,因为 Kuberneters 内几乎所有数据都会存放在 etcd 节点里,它是维持高可用的必须条件。这 3 台最好分别位于不同的机器,避免一台主机的停止导致整个 Kuberneters 不可用

  • 2 台 master,最好分别位于不同的机器

  • 若干 worker

部署步骤

vSphere

  • 配置 deploy 机器,设定好 deploy 机器的 IP 地址,以及计算机名后,运行脚本 start_deploy.sh,脚本内容如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
ansible-playbook -i ./inventory playbook.yml

playbook.yml
- hosts: all
become: yes
remote_user: root
tasks:
- name: 删除原有dns
file:
path: /etc/resolv.conf
state: absent

- name: 软链接阻止重新生成
file:
path: /etc/resolv.conf
src: /run/systemd/resolve/resolv.conf
state: link

- name: 临时修改主机名
shell: hostname {{ inventory_hostname }}

- name: 永久修改主机名
lineinfile:
path: /etc/hostname
regexp: '.*'
line: "{{ inventory_hostname }}"

# - name: 配置每台主机的hosts(/etc/hosts),添加host_ip $hostname到/etc/hosts文件中
# template:
# src: hosts.j2
# dest: /etc/hosts
# mode: '0644'

- name: 关闭防火墙
shell: ufw disable

- name: 修改时区为国内
shell: ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

- name: apt-get update
shell: apt-get update

- name: Install packages to allow apt to use a repository over HTTPS
shell: apt-get install -y apt-transport-https ca-certificates curl gnupg-agent software-properties-common

- name: Add Docker’s official GPG key
shell: curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

- name: Verify
shell: apt-key fingerprint 0EBFCD88

- name: Additional install
shell: apt install apt-transport-https ca-certificates curl software-properties-common

- name: Additional key
shell: curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

- name: Additional responsity
shell: add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic test"

- name: apt-get update
shell: apt-get update

- name: set docker version
shell: apt-cache madison docker-ce|grep {{ DOCKER_VERSION }}|awk '{print $3}'
register: version

- name: final install
shell: apt-get install -y docker-ce={{ version.stdout }} --allow-downgrades

- name: start automatic
shell: systemctl enable docker

  • 通过 ssh 登录到 deploy 机器上,运行以下命令,假设 deploy 机器位于 192.168.1.199

docker run -d --restart=unless-stopped --name=rancher-server --log-driver json-file --log-opt max-size=5g -p 80:80 -p 443:443 -v 192.168.1.199:/var/lib/rancher/ -v /root/var/log/auditlog:/var/log/auditlog -e AUDIT_LEVEL=3 rancher/rancher:v2.2.4

  • 等待一段时间后在内网任意机器上,直接访问 https://192.168.1.199

  • 右上角头像账户 -> Cloud Credentials -> Add Cloud Credential,以下假设 vSphere 的地址为 192.168.1.140

Name: credential(可任意填)

Cloud Credential Type: VMware vSphere

vCenter or ESXi Server: 192.168.1.140

Port: 443

Username: ***

Password: ***

  • 点击 Create

  • 右上角头像账户 -> Node Template -> Add Template -> 选择 vSphere

  • Account Access 选择刚刚创建的 credential

  • Instance Options

配置按需填写

  • Scheduling

这里的填写模式,应该去看 vSphere Client -> Shortcuts -> Global Inventory Lists 里的详细列表。不要看 rancher 的提示

注意 HostPool 互斥,写了一个就不要写第二个

Folder: /rancher-k8s(指定虚拟机文件夹,仅是为了方便管理,可以不填)

  • 创建完毕一个 Node Template 后,可以按需要 Clone 多个,然后只是微调下硬件配置,或调整 Host 配置,以便达到开设不同硬件类型机器、强制部署不同主机的目的

  • 返回 rancher 首页,点击 Add Cluster,选择 vSphere

  • 按需设置 Node Pools,注意 Name Prefix 里不可以带下划线,建议使用 rancher-k8s-workerrancher-k8s-master 等。并且,按最佳实践,应该在刚刚一步 Clone 多个 Node Template,保证 etcd 位于不同机器,保证 master 位于不同机器。后续需要添加 Node 时,也是以不同的 Node Template 为单位新增的

  • 设置 Cluster Options

Kubernetes Version: 1.13.5-rancher1-3

Network Provider: Flannel (这种网络格式比较简单且稳定)

Cloud Provider: Custom,然后点击Edit,将本项目内的文件cloud-config.yml里的内容复制进去

注意部署完毕后,需要再次去检查 cloud-config.yml ,曾经遇到过 insecure_flag 被重新设置为 false 的情况,如果发生此情况则集群无法成功创建,需要重新 Edit Cluster。以下是 cloud-config.yml 内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
addon_job_timeout: 30
authentication:
strategy: "x509|webhook"
bastion_host:
ssh_agent_auth: false
cloud_provider:
name: "vsphere"
vsphereCloudProvider:
global:
insecure-flag: true
soap-roundtrip-count: 0
virtual_center:
192.168.1.140:
datacenters: "***"
soap-roundtrip-count: 0
user: ***
password: ***
workspace:
datacenter: "***"
default-datastore: "***"
folder: "***"
server: "192.168.1.140"
ignore_docker_version: true
ingress:
provider: "nginx"
kubernetes_version: "v1.13.5-rancher1-3"
monitoring:
provider: "metrics-server"
network:
options:
flannel_backend_type: "vxlan"
plugin: "flannel"
restore:
restore: false
services:
etcd:
backup_config:
enabled: true
interval_hours: 12
retention: 6
creation: "12h"
extra_args:
election-timeout: "5000"
heartbeat-interval: "500"
retention: "72h"
snapshot: false
kube-api:
always_pull_images: false
pod_security_policy: false
service_node_port_range: "30000-32767"
kubelet:
fail_swap_on: false
ssh_agent_auth: false
#
# # Rancher Config
#
docker_root_dir: "/var/lib/docker"
enable_cluster_alerting: false
enable_cluster_monitoring: false
enable_network_policy: false
local_cluster_auth_endpoint:
enabled: true
name: "rancher"

  • 点击 Create,进入首页 -> rancher -> Nodes,查看创建进程,可能需要等待数分钟。也可以登录到 deploy 机器,使用 docker logs rancher-server -f 命令,实时查看 rancher 当前的创建进度

  • 一旦修改集群配置,集群内所有服务将不可用,但添加Node节点不影响,尽可能避免删除Node节点

  • 当集群成功运行时,运行命令 kubectl apply -f ./k8s-storageclass.yaml(如果没有安装并配置 kubectl, 请参见后面的 [macOS 的配置] 一节),该命令会添加自动请求磁盘空间的 StorageClass: eudicssdeudichdd。可以让服务后续自由的选择硬盘性质

普通机器

  • 将需要部署的机器的IP地址全部填入 inventory 文件的 [deploy] 下,这些机器必须满足能够在本机直接免密码登录root账户,且互相之间支持免密码登录root账户

  • 在 macOS 环境下,运行 start_deploy.sh 脚本。它需要 ansible 作为基础。如果没有 ansible,则可以 pip install ansible

  • start_deploy.sh 的作用是给每台机器配置可以完美运行 rancher 的环境,包括安装指定的 Docker、设定 Host 等

  • start_deploy.sh 是基于 Ubuntu 18.04 LTS 制作的

  • 如果后续需要添加新的机器,则这些新机器也必须要跑一遍 start_deploy.sh,然后再做下面的操作

  • 指定一台机器为运行 rancher的机器。在这台机器里运行以下指令,假设它位于 192.168.1.199

docker run -d --restart=unless-stopped -p 80:80 -p 443:443 -v 192.168.1.199:/var/lib/rancher/ -v /root/var/log/auditlog:/var/log/auditlog -e AUDIT_LEVEL=3 rancher/rancher:v2.2.4

  • 等待一段时间后直接访问 https://192.168.1.199

  • 点击 Add Cluster,选择 Custom

  • 设置 Cluster Options

Kubernetes Version: 1.13.5-rancher1-3

Network Provider: Flannel

  • 如果不是云服务器,保持 Cloud Provider 为空

  • 点击 Next

  • 此时会出现一段命令,需要手工选择节点的角色,会自动出现需要的命令,此时需要自己手工登录这些机器,并运行指定命令。Rancher 会自动检测内网里已运行命令的机器

  • 观察 rancher,直到所有 Node 状态都是已完成

外部服务发现的必要操作

由于目前常规服务(非 gRPC 服务)是通过 Ingress -> Service -> Deployment/StatefulSet -> Pod 的流程来找到具体的服务并发送接收流量的。Ingress 会根据域名来跳转。因此需要配置内置 DNS 服务,支持通配符匹配域名并跳转指定 IP,以下是 pfsense 的配置方法

需要注意的是:直接在 Host Overrides 里,让 Host 留空并只写 Parent domain of host 是无法起到通配符匹配的效果的

  • 进入 pfsense -> Services -> DNS Resolver -> General Settings -> Custom options

  • 在 Custom opptions 中输入以下内容,以便 test.local 的流量指向 worker

1
2
3
server:
local-zone: "test.local" redirect
local-data: "test.local 86400 IN A 192.168.1.226"

安装组件

Kubernetes Dashboard

  • 可以直接在 Rancher 的 App 内安装

  • 注意 Dashboard 必须安装在 System 命名空间内。可以打开 Enable Dashboard Cluster Admin Role,这样可以避免查询 Token 才能再次登录的问题

Helm(可选,暂时不需要)

  • 进入 Global,点击 Tools -> Catalogs,开启 Helm Stable,不开启 Helm Incubator,保证安装的都是 stable。

  • 安装 Helm CLI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
snap install helm --classic
kubectl -n kube-system create serviceaccount tiller

kubectl create clusterrolebinding tiller \
--clusterrole=cluster-admin \
--serviceaccount=kube-system:tiller

#helm init --service-account tiller

# Users in China: You will need to specify a specific tiller-image in order to initialize tiller.
# The list of tiller image tags are available here: https://dev.aliyun.com/detail.html?spm=5176.1972343.2.18.ErFNgC&repoId=62085.
# When initializing tiller, you'll need to pass in --tiller-image

helm init --service-account tiller --tiller-image registry.cn-hangzhou.aliyuncs.com/google_containers/tiller:v2.13.1
  • 完成后,做一下验证
    helm version

应该显示

1
2
3
4

Client: &version.Version{SemVer:"v2.14.0", GitCommit:"05811b84a3f93603dd6c2fcfe57944dfa7ab7fd0", GitTreeState:"clean"}
Server: &version.Version{SemVer:"v2.13.1", GitCommit:"618447cbf203d147601b4b9bd7f8c37a5d39fbb4", GitTreeState:"clean"}

macOS 的配置

  • 安装 kubectl,这样可以本地运行 kubectl 直接部署需要的服务

brew install kubernetes-cli

  • 安装 kompose,这样可以将现存的 docker-compose.yml 直接转换成 Kubernetes 支持的 yaml

brew install kompose

  • 本地开设一个文件位于 ~/.kube/config,然后进入这里找到配置好的 config 文件: Cluster -> Kubeconfig File 内。配置成功后,本地调用 kubectl get nodes 进行验证。如果出现了服务器里的 nodes 则说明一切正常。

  • 注意在使用 kubectl 之前,必须确保关闭了Terminal代理(代理软件可以保持打开)。否则会反复报EOF错误。如有必要,运行以下命令清空环境变量

1
2
unset http_proxy
unset https_proxy

已知问题

  • 目前常规服务(非 gRPC 服务)是通过 Ingress -> Service -> Deployment/StatefulSet -> Pod 的流程来找到具体的服务并发送接收流量的。Ingress 会根据域名来跳转到合适的服务里,如果 pfsense 绑定的 IP 地址所在的 Node 不可用,那么将会导致域名解析到已经停止的机器上,进而导致服务无法访问。但实际上是可以访问的

  • 如果某个服务需要开设大量端口给 Kubernetes 外部使用,那么必须使用 NodePort 来实现,不太友好

  • 目前 Node 不可用后,需要等待 5 分钟才能调度该 Node 里的 Pod 到新的 Node 里去,时间太长

  • vSphere 不支持增大硬盘空间,一旦创建则不可修改。云服务则是可以的。目前只能通过指定较大的硬盘空间,来规避此问题

  • 目前创建会提示 Error creating machine: Error in driver during machine creation: The host does not have sufficient CPU resources to satisfy the reservation.

目前最新版已经是 8.x,但 Solr 似乎会平行发布版本。比如6.x的版本仍然会在7.7.1发布后更新

粗略的基础知识

  • Solr 中的 Core 可以理解成一个单独的 Database

  • managed-schema 的意义是配置当前的Core的字段,以及分字段的单词分析,例如 text_en 等。注意必须提前配置好,才能进行数据的导入工作,否则数据即使导入成功也无法按指定的索引流程执行分词

  • solrconfig.xml 的意义是配置全局 Solr,一般只是添加 queryParser,这样可以使用自定义的 QParserPlugin

部署

  • 部署的策略是全部采用 Docker,将所有配置文件全部单独做一个 Docker Image。这样的目的是为了完全分离数据与配置,同时方便打包

可直接在 root权限下运行的 Dockerfile 参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
FROM solr:7.7.1

# 强制改成 root 运行
USER root

# 设置默认的 Core
RUN mkdir -p /opt/solr/server/solr/test1/data && mkdir -p /opt/solr/server/solr/test2/data && mkdir -p /opt/solr/server/solr/test3/data && cp -r /opt/solr/server/solr/configsets/_default/conf /opt/solr/server/solr/test1/ && cp -r /opt/solr/server/solr/configsets/_default/conf /opt/solr/server/solr/test2/ && cp -r /opt/solr/server/solr/configsets/_default/conf /opt/solr/server/solr/test3/

# 设置用户名与密码
COPY security.json /opt/solr/server/solr

# 设置Jetty并发数
COPY jetty.xml /opt/solr/server/etc

EXPOSE 8877

CMD ["solr-foreground", "-f", "-force"]

# 调试用命令,注意IP地址需要与服务器地址完全一致
#CMD ["solr-foreground", "-f", "-force", "-a", "-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=10.117.2.33:18988"]

自定义 TokenFilter

TokenFilter 的作用,是用来针对一句话做最终的关键字筛选。
例如一句话是 I eat apple。如果使用默认的 text_en,那么最终结果就是 eat apple,而 I 会被隐藏
如果希望 eat 是作为 ate 来索引关键字,或者 apple这个关键字需要其他特殊处理,就需要自定义 TokenFilter

自定义 QParserPlugin

为了能够满足项目要求的排序,可能原生的 boost 无法满足要求时

一些建议

  • 原始的 solrconfig.xml 内包含大量预定义配置,请删除用不到的,只保留用得到的,不要保留模棱两可的配置

近期发现公司项目中的视频文件 seek 不准,误差在 0.1s~0.2s之间
并且已经使用了以下方法:

  • 全部转成 CMTime 来 seek

  • seekToTime:toleranceBefore:toleranceAfter: 方法的前后参数全部是 kCMTimeZero

该问题是跨平台问题,,在 macOS下也可以复现。

如果采用 ffmpeg,则 seek 准确。但项目是基于 AVPlayer 写的,已不可能全部改成ffmpeg。因此考虑编码问题

经过测试,以下编码可以使得 AVPlayer 的 seek 精确。其原理可能是重新分配了关键帧

ffmpeg -i {原始mp4} -vcodec libx264 -x264-params min-keyint=24 -force_key_frames "expr:gte(t,n_forced*1)" {输出mp4}

项目内使用,可支持写一次 Vue,部署到小程序、浏览器、Electron 上。

项目地址:[https://github.com/youngcube/mpvue-html-electron-typescript-starter]

Features:

  • 使用 TypeScript(感谢 @WingGao)[https://github.com/WingGao/mpvue-ts-demo]
  • 支持 vue-router 写法(感谢 @F-loat)[https://github.com/F-loat/mpvue-router-patch]
  • 支持直接打包输出为 小程序 与 HTML、支持热更新
  • 如果有多个类似项目,支持一键导出这些项目
  • 配置完善的 Visual Studio Code 参数,可直接在 Visual Studio Code 内点击运行并在 Visual Studio Code 内断点调试 HTML

编译

1
yarn

或者

1
npm install

运行

支持在命令行运行,或直接 Visual Studio Code 点击运行

命令行运行方法

  • 浏览器

    npm run dev:web

  • 小程序

    npm run dev:wechat:red,其中:red为语言,可选redblue`

Visual Studio Code 运行方法

  • 可以在 Visual Studio CodeDebug 侧边栏直接点击运行 Run & Debug Web / Run Wechat
  • 其中 Run & Debug Web 支持直接运行需要的 npm 命令,并在服务启动后自动打开 Chrome 并在 Visual Studio Code 内直接进行调试;此时运行的 Chrome 为隐私模式,无任何 Chrome 插件
  • 其中 Attach Chrome Port 9222 需要手工在 Terminal 运行 Chrome:/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222;此时运行的 Chrome 会带有 Chrome 自己的所有插件。
  • 小程序必须使用它的框架开发者工具包,不支持上述简便调试方法。必须在微信开发者工具内调试
  • 其中 Launch Webpack ... 可用于调试 Webpack 的打包问题

打包

浏览器(打包 red 项目)
npm run build:web:red

小程序(打包 blue 项目)
npm run build:wechat:blue

IDE

  • 推荐使用 Visual Studio Code,并安装本工程推荐的所有依赖,这样可以直接按指定的(ESLint)标准格式进行保存,同时可以直接在 Visual Studio Code 内进行调试 Web 页面

Vue Single File Components (SFC)

  • 不要在 SFC 内的 <script> 标签 和 <style> 标签直接写 TypeScript / JavaScript / LESS,请新建文件然后用 src 引入。因为内部的<script>标签不支持直接写 TypeScript(写了以后无法渲染 components);IDE 目前也不支持 *.vue 内的 TypeScript 检查组件属性类型。因此尽可能使用 src/ @import 引入。这样也方便重构。

入口

  • 浏览器:app-web.vue,main-web.ts
  • 小程序:app-web.vue,main.ts

其他注意事项

  • 目前(2018.08) mpvue 不支持 webpack 4 issue98。请勿随意使用 yarn 升级,可能导致小程序或 H5 端编不过或其他奇怪的问题

  • 使用 wx 来判断是否在小程序环境内

  • 请注意 HTML 与 小程序有较多不同之处

某款 macOS App 发布更新后,经常有用户反馈App闪退。拿到闪退报告后,发现都是在 10.11 以下系统中出现的,遂先在本地利用虚拟机安装了一个老版本的 macOS,发现问题成功复现。问题报告如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
System Integrity Protection: enabled

Crashed Thread: 0 Dispatch queue: com.apple.main-thread

Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Codes: EXC_I386_GPFLT
Exception Note: EXC_CORPSE_NOTIFY


....

Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
0 libobjc.A.dylib 0x00007fff90f424e9 objc_msgSend + 41
1 com.apple.AppKit 0x00007fff8e70cdb9 -[NSTableView _isGroupRow:] + 81
2 com.apple.AppKit 0x00007fff8e7125bf -[NSTableRowData _setPropertiesForRowView:atRow:] + 626
3 com.apple.AppKit 0x00007fff8e712194 -[NSTableRowData _initializeRowView:atRow:] + 371
4 com.apple.AppKit 0x00007fff8e710907 -[NSTableRowData _addRowViewForVisibleRow:withPriorView:] + 416
5 com.apple.AppKit 0x00007fff8e71069b -[NSTableRowData _addRowViewForVisibleRow:withPriorRowIndex:inDictionary:withRowAnimation:] + 299
6 com.apple.AppKit 0x00007fff8e70f3b2 -[NSTableRowData _unsafeUpdateVisibleRowEntries] + 1522
7 com.apple.AppKit 0x00007fff8e70ed22 -[NSTableRowData updateVisibleRowViews] + 233
8 com.apple.AppKit 0x00007fff8e8ed54a -[NSTableRowData prepareContentInRect:] + 75
9 com.apple.AppKit 0x00007fff8e8ed096 -[NSTableView prepareContentInRect:] + 289
10 com.apple.AppKit 0x00007fff8e78f4c4 __48-[NSView _pullInTilesByDrawingAndAddingSubviews]_block_invoke + 601
11 com.apple.AppKit 0x00007fff8e78ebf0 -[NSView _performWorkOnTilesFromRect:renderedContentRect:maximumRect:scrollVelocity:handler:] + 2407
12 com.apple.AppKit 0x00007fff8e78e064 -[NSView _pullInTilesByDrawingAndAddingSubviews] + 1017
13 com.apple.AppKit 0x00007fff8e78dc61 -[NSView _pullInExtraTilesForOverdraw] + 87
14 com.apple.AppKit 0x00007fff8e78dbdd -[NSView _doIdlePrefetch] + 37
15 com.apple.AppKit 0x00007fff8e745410 __CreateIdleRunLoopObserverWithHandler_block_invoke + 236
16 com.apple.CoreFoundation 0x00007fff91a7cfc7 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 23
17 com.apple.CoreFoundation 0x00007fff91a7cf37 __CFRunLoopDoObservers + 391
18 com.apple.CoreFoundation 0x00007fff91a5c52a __CFRunLoopRun + 1178
19 com.apple.CoreFoundation 0x00007fff91a5be28 CFRunLoopRunSpecific + 296
20 com.apple.HIToolbox 0x00007fff9f174935 RunCurrentEventLoopInMode + 235
21 com.apple.HIToolbox 0x00007fff9f17476f ReceiveNextEventCommon + 432
22 com.apple.HIToolbox 0x00007fff9f1745af _BlockUntilNextEventMatchingListInModeWithFilter + 71
23 com.apple.AppKit 0x00007fff8e5f8df6 _DPSNextEvent + 1067
24 com.apple.AppKit 0x00007fff8e5f8226 -[NSApplication _nextEventMatchingEventMask:untilDate:inMode:dequeue:] + 454
25 com.apple.AppKit 0x00007fff8e5ecd80 -[NSApplication run] + 682
26 com.apple.AppKit 0x00007fff8e5b6368 NSApplicationMain + 1176
27 libdyld.dylib 0x00007fff95f355ad start + 1

....

查看 Git 记录

一般来说,低版本内出现的问题,通常都是不正确的使用了高版本的 API 导致的。可以参考:macOS 低版本内误引用高版本API导致的 __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ 闪退。但是,该闪退版本主要目标,是为了将原有老的 TUITableView 改写成新的 NSTableView。改写时,已经翻阅过 API 文档,确认了没有误用低 API。因此误用 API 可以排除。

查找问题

由于是可以稳定复现的,但比较麻烦的是只能在低版本复现,因此只能一点点为Delegate方法添加 NSAlert ,通过是否弹出警告框,来验证是走到了哪一步闪退。由于闪退时的堆栈信息最后位于内部方法 [NSTableView _isGroupRow:]上,因此先从 - (BOOL)outlineView:(NSOutlineView *)outlineView isGroupItem:(id)item 开始。

结果证明,程序在跑完 - (BOOL)outlineView:(NSOutlineView *)outlineView isGroupItem:(id)item后,才会闪退。那么再一次缩小范围,将- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(nullable id)item 中加入 NSAlert ,程序没有出现警告框,直接就闪退了。

那么现在问题就集中在 - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(nullable id)item 这个代理方法上了。该方法用于返回一个任意的指针,来告知 NSOutlineView 子部件需要显示的内容。

这里返回的 id(对于一般的业务逻辑来说,一般都是 NSDictionary,这里也是会返回一个 NSDictionary),我都已经提前生成好,并且强引用了。但为了图方便,我在调用这个方法时,又生成了一个新的数据写入 NSDictionary。 通常来说,这里返回的 id ,是应该在本地被强引用的。因为 Cocoa 一般的 Protocol API 都不会强引用这些。问题可能位于此。

解决问题

- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(nullable id)item中,不对返回的 NSDictionary 进行任何修改。即使实在需要修改,也必须同时修改本地强引用的变量。如此操作后,在10.11下验证,问题消失,不再会出现闪退情况

NSSearchField 的 delegate 是 10.10 以上方法。如果将该方法使用,并运行到了 10.9 或以下机型内,会报如下错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
0 com.eusoft.eudic 0x000000010d093f3a 0x10cfa2000 + 991034
1 com.eusoft.eudic 0x000000010d0922c6 0x10cfa2000 + 983750
2 com.eusoft.eudic 0x000000010d09095e 0x10cfa2000 + 977246
3 com.eusoft.eudic 0x000000010d090c98 0x10cfa2000 + 978072
4 com.apple.CoreFoundation 0x00007fff965a4e0c __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ + 12
5 com.apple.CoreFoundation 0x00007fff9649882d _CFXNotificationPost + 2893
6 com.apple.Foundation 0x00007fff922c4e4a -[NSNotificationCenter postNotificationName:object:userInfo:] + 68
7 com.eusoft.eudic 0x000000010d0d562d 0x10cfa2000 + 1259053
8 com.apple.CoreFoundation 0x00007fff965a4e0c __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ + 12
9 com.apple.CoreFoundation 0x00007fff9649882d _CFXNotificationPost + 2893
10 com.apple.Foundation 0x00007fff922c4e4a -[NSNotificationCenter postNotificationName:object:userInfo:] + 68
11 com.apple.AppKit 0x00007fff92726abf -[NSTableView _sendSelectionChangedNotificationForRows:columns:] + 177
12 com.apple.AppKit 0x00007fff92653354 -[NSTableView _enableSelectionPostingAndPost] + 406
13 com.apple.AppKit 0x00007fff92726427 -[NSTableView _doSelectIndexes:byExtendingSelection:indexType:funnelThroughSingleIndexVersion:] + 2706
14 com.eusoft.eudic 0x000000010d0d34b8 0x10cfa2000 + 1250488
15 com.eusoft.eudic 0x000000010d08f3d8 0x10cfa2000 + 971736
16 com.apple.AppKit 0x00007fff925d5b3f -[NSIBObjectData nibInstantiateWithOwner:topLevelObjects:] + 1223
17 com.apple.AppKit 0x00007fff92679e5c -[NSNib _instantiateNibWithExternalNameTable:] + 600
18 com.apple.AppKit 0x00007fff927a9482 -[NSNib instantiateNibWithOwner:topLevelObjects:] + 215
19 com.apple.AppKit 0x00007fff927a9262 -[NSViewController loadView] + 180
20 com.apple.AppKit 0x00007fff926d7718 -[NSViewController view] + 41
21 com.eusoft.eudic 0x000000010d11e1f0 0x10cfa2000 + 1556976
22 com.apple.AppKit 0x00007fff92d1c099 -[NSToolbarButton sendAction:to:] + 75
23 com.apple.AppKit 0x00007fff92d1c0e8 -[NSToolbarButton sendAction] + 65
24 com.apple.AppKit 0x00007fff928cff0c -[NSToolbarItemViewer mouseDown:] + 4897
25 com.apple.AppKit 0x00007fff927eba58 -[NSWindow sendEvent:] + 11296
26 com.apple.AppKit 0x00007fff9278a5d4 -[NSApplication sendEvent:] + 2021
27 com.apple.AppKit 0x00007fff925da9f9 -[NSApplication run] + 646
28 com.apple.AppKit 0x00007fff925c5783 NSApplicationMain + 940
29 libdyld.dylib 0x00007fff96a765fd start + 1

推测可能是 Cocoa 内部的 Protocol ,仍然大部分用的是 NSNotification 实现,导致出错的堆栈提示的是 __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__

隐私政策

youngcube尊重并保护所有使用服务用户的个人隐私权。为了给您提供更准确、更有个性化的服务,youngcube会按照本隐私权政策的规定使用和披露您的个人信息。但youngcube将以高度的勤勉、审慎义务对待这些信息。除本隐私权政策另有规定外,在未征得您事先许可的情况下,youngcube不会将这些信息对外披露或向第三方提供。youngcube会不时更新本隐私权政策。 您在同意youngcube服务使用协议之时,即视为您已经同意本隐私权政策全部内容。本隐私权政策属于youngcube服务使用协议不可分割的一部分。

  1. 适用范围

a) 在您注册youngcube帐号时,您根据youngcube要求提供的个人注册信息;

b) 在您使用youngcube网络服务,或访问youngcube平台网页时,youngcube自动接收并记录的您的浏览器和计算机上的信息,包括但不限于您的IP地址、浏览器的类型、使用的语言、访问日期和时间、软硬件特征信息及您需求的网页记录等数据;

c) youngcube通过合法途径从商业伙伴处取得的用户个人数据。

您了解并同意,以下信息不适用本隐私权政策:

a) 您在使用youngcube平台提供的搜索服务时输入的关键字信息;

b) youngcube收集到的您在youngcube发布的有关信息数据,包括但不限于参与活动、成交信息及评价详情;

c) 违反法律规定或违反youngcube规则行为及youngcube已对您采取的措施。

  1. 信息使用

a) youngcube不会向任何无关第三方提供、出售、出租、分享或交易您的个人信息,除非事先得到您的许可,或该第三方和youngcube(含youngcube关联公司)单独或共同为您提供服务,且在该服务结束后,其将被禁止访问包括其以前能够访问的所有这些资料。

b) youngcube亦不允许任何第三方以任何手段收集、编辑、出售或者无偿传播您的个人信息。任何youngcube平台用户如从事上述活动,一经发现,youngcube有权立即终止与该用户的服务协议。

c) 为服务用户的目的,youngcube可能通过使用您的个人信息,向您提供您感兴趣的信息,包括但不限于向您发出产品和服务信息,或者与youngcube合作伙伴共享信息以便他们向您发送有关其产品和服务的信息(后者需要您的事先同意)。

  1. 信息披露

在如下情况下,youngcube将依据您的个人意愿或法律的规定全部或部分的披露您的个人信息:

a) 经您事先同意,向第三方披露;

b) 为提供您所要求的产品和服务,而必须和第三方分享您的个人信息;

c) 根据法律的有关规定,或者行政或司法机构的要求,向第三方或者行政、司法机构披露;

d) 如您出现违反中国有关法律、法规或者youngcube服务协议或相关规则的情况,需要向第三方披露;

e) 如您是适格的知识产权投诉人并已提起投诉,应被投诉人要求,向被投诉人披露,以便双方处理可能的权利纠纷;

f) 在youngcube平台上创建的某一交易中,如交易任何一方履行或部分履行了交易义务并提出信息披露请求的,youngcube有权决定向该用户提供其交易对方的联络方式等必要信息,以促成交易的完成或纠纷的解决。

g) 其它youngcube根据法律、法规或者网站政策认为合适的披露。

  1. 信息存储和交换

youngcube收集的有关您的信息和资料将保存在youngcube及(或)其关联公司的服务器上,这些信息和资料可能传送至您所在国家、地区或youngcube收集信息和资料所在地的境外并在境外被访问、存储和展示。

  1. Cookie的使用

a) 在您未拒绝接受cookies的情况下,youngcube会在您的计算机上设定或取用cookies

,以便您能登录或使用依赖于cookies的youngcube平台服务或功能。youngcube使用cookies可为您提供更加周到的个性化服务,包括推广服务。 b) 您有权选择接受或拒绝接受cookies。您可以通过修改浏览器设置的方式拒绝接受cookies。但如果您选择拒绝接受cookies,则您可能无法登录或使用依赖于cookies的youngcube网络服务或功能。

c) 通过youngcube所设cookies所取得的有关信息,将适用本政策。

  1. 信息安全

a) youngcube帐号均有安全保护功能,请妥善保管您的用户名及密码信息。youngcube将通过对用户密码进行加密等安全措施确保您的信息不丢失,不被滥用和变造。尽管有前述安全措施,但同时也请您注意在信息网络上不存在“完善的安全措施”。

b) 在使用youngcube网络服务进行网上交易时,您不可避免的要向交易对方或潜在的交易对方披露自己的个人信息,如联络方式或者邮政地址。请您妥善保护自己的个人信息,仅在必要的情形下向他人提供。如您发现自己的个人信息泄密,尤其是youngcube用户名及密码发生泄露,请您立即联络youngcube客服,以便youngcube采取相应措施。