
软件开发入门教程网之Git 基本操作
Git 基本操作
Git 的工作就是创建和保存你项目的快照及与之后的快照进行对比。
本章将对有关创建与提交你的项目快照的命令作介绍。
Git 常用的是以下 6 个命令:git clone 、git push 、git add 、git commit 、git checkout 、git pull,后面我们会详细介绍。
说明:
workspace:工作区
staging area:暂存区/缓存区
local repository:版本库或本地仓库
remote repository:远程仓库
一个简单的操作步骤:
$ git init
$ git add .
$ git commit
git init - 初始化仓库。
git add . - 添加文件到暂存区。
git commit - 将暂存区内容添加到仓库中。
创建仓库命令
下表列出了 git 创建仓库的命令:
命令 说明
git init 初始化仓库
git clone 拷贝一份远程仓库,也就是下载一个项目。
提交与修改
Git 的工作就是创建和保存你的项目的快照及与之后的快照进行对比。
下表列出了有关创建与提交你的项目的快照的命令:
命令 说明
git add 添加文件到暂存区
git status 查看仓库当前的状态,显示有变更的文件。
git diff 比较文件的不同,即暂存区和工作区的差异。
git commit 提交暂存区到本地仓库。
git reset 回退版本。
git rm 删除工作区文件。
git mv 移动或重命名工作区文件。
提交日志
命令 说明
git log 查看历史提交记录
git blame 以列表形式查看指定文件的历史修改记录
远程操作
命令 说明
git remote 远程仓库操作
git fetch 从远程获取代码库
git pull 下载远程代码并合并
git push 上传远程代码并合并
Git 基本操作
Git 的工作就是创建和保存你项目的快照及与之后的快照进行对比。
本章将对有关创建与提交你的项目快照的命令作介绍。
Git 常用的是以下 6 个命令:git clone 、git push 、git add 、git commit 、git checkout 、git pull,后面我们会详细介绍。
说明:
workspace:工作区
staging area:暂存区/缓存区
local repository:版本库或本地仓库
remote repository:远程仓库
一个简单的操作步骤:
$ git init
$ git add .
$ git commit
git init - 初始化仓库。
git add . - 添加文件到暂存区。
git commit - 将暂存区内容添加到仓库中。
创建仓库命令
下表列出了 git 创建仓库的命令:
命令 说明
git init 初始化仓库
git clone 拷贝一份远程仓库,也就是下载一个项目。
提交与修改
Git 的工作就是创建和保存你的项目的快照及与之后的快照进行对比。
下表列出了有关创建与提交你的项目的快照的命令:
命令 说明
git add 添加文件到暂存区
git status 查看仓库当前的状态,显示有变更的文件。
git diff 比较文件的不同,即暂存区和工作区的差异。
git commit 提交暂存区到本地仓库。
git reset 回退版本。
git rm 删除工作区文件。
git mv 移动或重命名工作区文件。
提交日志
命令 说明
git log 查看历史提交记录
git blame 以列表形式查看指定文件的历史修改记录
远程操作
命令 说明
git remote 远程仓库操作
git fetch 从远程获取代码库
git pull 下载远程代码并合并
git push 上传远程代码并合并

运行到ios 提示(打开文件服务失败,请尝试拔掉数据线后重新连接手机,或重启手机再试),报错,重新生成证书profile文件
今天运行项目到ios手机,测试包可以安装,hbulider运行提示如下报错:打开文件服务失败,请尝试拔掉数据线后重新连接手机,或重启手机再试(请确认自定义基座的iOS证书已添加iOS设备的UDID;如果是IOS15.1以上的设备,请确认基座版本号为3.3.2及以上)
解决:重新生成profile证书,重新生成自定义基座,重新运行
今天运行项目到ios手机,测试包可以安装,hbulider运行提示如下报错:打开文件服务失败,请尝试拔掉数据线后重新连接手机,或重启手机再试(请确认自定义基座的iOS证书已添加iOS设备的UDID;如果是IOS15.1以上的设备,请确认基座版本号为3.3.2及以上)
解决:重新生成profile证书,重新生成自定义基座,重新运行
收起阅读 »
软件开发入门教程网之MySQL NULL 值处理
MySQL NULL 值处理
我们已经知道 MySQL 使用 SQL SELECT 命令及 WHERE 子句来读取数据表中的数据,但是当提供的查询条件字段为 NULL 时,该命令可能就无法正常工作。
为了处理这种情况,MySQL提供了三大运算符:
IS NULL: 当列的值是 NULL,此运算符返回 true。
IS NOT NULL: 当列的值不为 NULL, 运算符返回 true。
<=>: 比较操作符(不同于 = 运算符),当比较的的两个值相等或者都为 NULL 时返回 true。
关于 NULL 的条件比较运算是比较特殊的。你不能使用 = NULL 或 != NULL 在列中查找 NULL 值 。
在 MySQL 中,NULL 值与任何其它值的比较(即使是 NULL)永远返回 NULL,即 NULL = NULL 返回 NULL 。
MySQL 中处理 NULL 使用 IS NULL 和 IS NOT NULL 运算符。
注意:
select * , columnName1+ifnull(columnName2,0) from tableName;
columnName1,columnName2 为 int 型,当 columnName2 中,有值为 null 时,columnName1+columnName2=null, ifnull(columnName2,0) 把 columnName2 中 null 值转为 0。
在命令提示符中使用 NULL 值
以下实例中假设数据库 RUNOOB 中的表 kxdang_test_tbl 含有两列 kxdang_author 和 kxdang_count, kxdang_count 中设置插入NULL值。
实例
尝试以下实例:
创建数据表 kxdang_test_tbl
root@host# mysql -u root -p password;
Enter password:***
mysql> use RUNOOB;
Database changed
mysql> create table kxdang_test_tbl
-> (
-> kxdang_author varchar(40) NOT NULL,
-> kxdang_count INT
-> );
Query OK, 0 rows affected (0.05 sec)
mysql> INSERT INTO kxdang_test_tbl (kxdang_author, kxdang_count) values ('RUNOOB', 20);
mysql> INSERT INTO kxdang_test_tbl (kxdang_author, kxdang_count) values ('菜鸟教程', NULL);
mysql> INSERT INTO kxdang_test_tbl (kxdang_author, kxdang_count) values ('Google', NULL);
mysql> INSERT INTO kxdang_test_tbl (kxdang_author, kxdang_count) values ('FK', 20);
mysql> SELECT * from kxdang_test_tbl;
+---------------+--------------+
| kxdang_author | kxdang_count |
+---------------+--------------+
| RUNOOB | 20 |
| 菜鸟教程 | NULL |
| Google | NULL |
| FK | 20 |
+---------------+--------------+
4 rows in set (0.01 sec)
以下实例中你可以看到 = 和 != 运算符是不起作用的:
mysql> SELECT FROM kxdang_test_tbl WHERE kxdang_count = NULL;
Empty set (0.00 sec)
mysql> SELECT FROM kxdang_test_tbl WHERE kxdang_count != NULL;
Empty set (0.01 sec)
查找数据表中 kxdang_test_tbl 列是否为 NULL,必须使用 IS NULL 和 IS NOT NULL,如下实例:
mysql> SELECT * FROM kxdang_test_tbl WHERE kxdang_count IS NULL;
+---------------+--------------+
| kxdang_author | kxdang_count |
+---------------+--------------+
| 菜鸟教程 | NULL |
| Google | NULL |
+---------------+--------------+
2 rows in set (0.01 sec)
mysql> SELECT * from kxdang_test_tbl WHERE kxdang_count IS NOT NULL;
+---------------+--------------+
| kxdang_author | kxdang_count |
+---------------+--------------+
| RUNOOB | 20 |
| FK | 20 |
+---------------+--------------+
2 rows in set (0.01 sec)
使用 PHP 脚本处理 NULL 值
PHP 脚本中你可以在 if...else 语句来处理变量是否为空,并生成相应的条件语句。
以下实例中 PHP 设置了 $kxdang_count 变量,然后使用该变量与数据表中的 kxdang_count 字段进行比较:
MySQL ORDER BY 测试:
<?php
$dbhost = 'localhost'; // mysql服务器主机地址
$dbuser = 'root'; // mysql用户名
$dbpass = '123456'; // mysql用户名密码
$conn = mysqli_connect($dbhost, $dbuser, $dbpass);
if(! $conn )
{
die('连接失败: ' . mysqli_error($conn));
}
// 设置编码,防止中文乱码
mysqli_query($conn , "set names utf8");
if( isset($kxdang_count ))
{
$sql = "SELECT kxdang_author, kxdang_count
FROM kxdang_test_tbl
WHERE kxdang_count = $kxdang_count";
}
else
{
$sql = "SELECT kxdang_author, kxdang_count
FROM kxdang_test_tbl
WHERE kxdang_count IS NULL";
}
mysqli_select_db( $conn, 'RUNOOB' );
$retval = mysqli_query( $conn, $sql );
if(! $retval )
{
die('无法读取数据: ' . mysqli_error($conn));
}
echo '<h2>菜鸟教程 IS NULL 测试<h2>';
echo '<table border="1"><tr><td>作者</td><td>登陆次数</td></tr>';
while($row = mysqli_fetch_array($retval, MYSQL_ASSOC))
{
echo "<tr>".
"<td>{$row['kxdang_author']} </td> ".
"<td>{$row['kxdang_count']} </td> ".
"</tr>";
}
echo '</table>';
mysqli_close($conn);
?>
输出结果如下图所示:
MySQL NULL 值处理
我们已经知道 MySQL 使用 SQL SELECT 命令及 WHERE 子句来读取数据表中的数据,但是当提供的查询条件字段为 NULL 时,该命令可能就无法正常工作。
为了处理这种情况,MySQL提供了三大运算符:
IS NULL: 当列的值是 NULL,此运算符返回 true。
IS NOT NULL: 当列的值不为 NULL, 运算符返回 true。
<=>: 比较操作符(不同于 = 运算符),当比较的的两个值相等或者都为 NULL 时返回 true。
关于 NULL 的条件比较运算是比较特殊的。你不能使用 = NULL 或 != NULL 在列中查找 NULL 值 。
在 MySQL 中,NULL 值与任何其它值的比较(即使是 NULL)永远返回 NULL,即 NULL = NULL 返回 NULL 。
MySQL 中处理 NULL 使用 IS NULL 和 IS NOT NULL 运算符。
注意:
select * , columnName1+ifnull(columnName2,0) from tableName;
columnName1,columnName2 为 int 型,当 columnName2 中,有值为 null 时,columnName1+columnName2=null, ifnull(columnName2,0) 把 columnName2 中 null 值转为 0。
在命令提示符中使用 NULL 值
以下实例中假设数据库 RUNOOB 中的表 kxdang_test_tbl 含有两列 kxdang_author 和 kxdang_count, kxdang_count 中设置插入NULL值。
实例
尝试以下实例:
创建数据表 kxdang_test_tbl
root@host# mysql -u root -p password;
Enter password:***
mysql> use RUNOOB;
Database changed
mysql> create table kxdang_test_tbl
-> (
-> kxdang_author varchar(40) NOT NULL,
-> kxdang_count INT
-> );
Query OK, 0 rows affected (0.05 sec)
mysql> INSERT INTO kxdang_test_tbl (kxdang_author, kxdang_count) values ('RUNOOB', 20);
mysql> INSERT INTO kxdang_test_tbl (kxdang_author, kxdang_count) values ('菜鸟教程', NULL);
mysql> INSERT INTO kxdang_test_tbl (kxdang_author, kxdang_count) values ('Google', NULL);
mysql> INSERT INTO kxdang_test_tbl (kxdang_author, kxdang_count) values ('FK', 20);
mysql> SELECT * from kxdang_test_tbl;
+---------------+--------------+
| kxdang_author | kxdang_count |
+---------------+--------------+
| RUNOOB | 20 |
| 菜鸟教程 | NULL |
| Google | NULL |
| FK | 20 |
+---------------+--------------+
4 rows in set (0.01 sec)
以下实例中你可以看到 = 和 != 运算符是不起作用的:
mysql> SELECT FROM kxdang_test_tbl WHERE kxdang_count = NULL;
Empty set (0.00 sec)
mysql> SELECT FROM kxdang_test_tbl WHERE kxdang_count != NULL;
Empty set (0.01 sec)
查找数据表中 kxdang_test_tbl 列是否为 NULL,必须使用 IS NULL 和 IS NOT NULL,如下实例:
mysql> SELECT * FROM kxdang_test_tbl WHERE kxdang_count IS NULL;
+---------------+--------------+
| kxdang_author | kxdang_count |
+---------------+--------------+
| 菜鸟教程 | NULL |
| Google | NULL |
+---------------+--------------+
2 rows in set (0.01 sec)
mysql> SELECT * from kxdang_test_tbl WHERE kxdang_count IS NOT NULL;
+---------------+--------------+
| kxdang_author | kxdang_count |
+---------------+--------------+
| RUNOOB | 20 |
| FK | 20 |
+---------------+--------------+
2 rows in set (0.01 sec)
使用 PHP 脚本处理 NULL 值
PHP 脚本中你可以在 if...else 语句来处理变量是否为空,并生成相应的条件语句。
以下实例中 PHP 设置了 $kxdang_count 变量,然后使用该变量与数据表中的 kxdang_count 字段进行比较:
MySQL ORDER BY 测试:
<?php
$dbhost = 'localhost'; // mysql服务器主机地址
$dbuser = 'root'; // mysql用户名
$dbpass = '123456'; // mysql用户名密码
$conn = mysqli_connect($dbhost, $dbuser, $dbpass);
if(! $conn )
{
die('连接失败: ' . mysqli_error($conn));
}
// 设置编码,防止中文乱码
mysqli_query($conn , "set names utf8");
if( isset($kxdang_count ))
{
$sql = "SELECT kxdang_author, kxdang_count
FROM kxdang_test_tbl
WHERE kxdang_count = $kxdang_count";
}
else
{
$sql = "SELECT kxdang_author, kxdang_count
FROM kxdang_test_tbl
WHERE kxdang_count IS NULL";
}
mysqli_select_db( $conn, 'RUNOOB' );
$retval = mysqli_query( $conn, $sql );
if(! $retval )
{
die('无法读取数据: ' . mysqli_error($conn));
}
echo '<h2>菜鸟教程 IS NULL 测试<h2>';
echo '<table border="1"><tr><td>作者</td><td>登陆次数</td></tr>';
while($row = mysqli_fetch_array($retval, MYSQL_ASSOC))
{
echo "<tr>".
"<td>{$row['kxdang_author']} </td> ".
"<td>{$row['kxdang_count']} </td> ".
"</tr>";
}
echo '</table>';
mysqli_close($conn);
?>
输出结果如下图所示:

2022最新上传ipa到appstore的步骤说明
最近有人提出问题,说IOS7怎么在APP store中下载软件,好多软件都提示需要ios8及以上才可以下载,而App Store里下载又不会提供给你旧版本,难倒ios7就必须升级才能下载吗?对此本人在网上查了好多资料也做了好多测试,大多数说的都是升级系统(这纯属废话,要是升级系统还用问你啊),终于皇天不负有心人,经过多次测试,我找到了一个解决办法,其实真的是非常简单,废话也不多说了,方法就是,把你的AppleId的用户名及密码让你的亲朋好友用他的苹果手机或者iPad登录(当然他的设备必须是高版本的)后先下载到他的设备上,这样你的AppleId就会记录下你的购买记录,接下来你就可以拿自己的低版本设备,用下载过该应用的AppleId登录,然后进入App Store 点击 最后一个栏目“更新”,在那里会显示当前AppleId下载过的应用,然后你点击你要下载的那个就可以了,当然也回提示当前应用版本需要IOS8及以上之类的,但是下面还有说 是否获取旧版本,这样就可以下载你当前设备可以下载的版本了。
证书我们这边可以借助辅助工具appuploader
Appuploader可以辅助在Windows、linux或mac系统直接申请iOS证书p12,及上传ipa到App Store,最方便在Windows开发上架没有苹果Mac电脑的开发者!配合本教程使用,可以快速掌握如何真机测试及上架!
点击苹果证书按钮
点击新增
输入证书密码,名称
这个密码不是账号密码,而是一个保护证书的密码,是p12文件的密码,此密码设置后没有其他地方可以找到,忘记了只能删除证书重新制作,所以请务必记住密码。还有为了安全起见,密码不要太简单。 证书名称是你为了在证书列表里面便于区别的一个字符,自己好辨识就可以,尽量是是字母和数字之类
选择证书类型
带distribution的是发布类型,带development的是开发类型。
apple类型=ios+mac,所以开发时选择ios app development和apple development 类型都是可以的
选择bundle id
只有部分类型的证书需要选择bundle id,例如推送证书。因为大部分证书是不和app关联的。而是通过描述文件profile文件关联app。
最近有人提出问题,说IOS7怎么在APP store中下载软件,好多软件都提示需要ios8及以上才可以下载,而App Store里下载又不会提供给你旧版本,难倒ios7就必须升级才能下载吗?对此本人在网上查了好多资料也做了好多测试,大多数说的都是升级系统(这纯属废话,要是升级系统还用问你啊),终于皇天不负有心人,经过多次测试,我找到了一个解决办法,其实真的是非常简单,废话也不多说了,方法就是,把你的AppleId的用户名及密码让你的亲朋好友用他的苹果手机或者iPad登录(当然他的设备必须是高版本的)后先下载到他的设备上,这样你的AppleId就会记录下你的购买记录,接下来你就可以拿自己的低版本设备,用下载过该应用的AppleId登录,然后进入App Store 点击 最后一个栏目“更新”,在那里会显示当前AppleId下载过的应用,然后你点击你要下载的那个就可以了,当然也回提示当前应用版本需要IOS8及以上之类的,但是下面还有说 是否获取旧版本,这样就可以下载你当前设备可以下载的版本了。
证书我们这边可以借助辅助工具appuploader
Appuploader可以辅助在Windows、linux或mac系统直接申请iOS证书p12,及上传ipa到App Store,最方便在Windows开发上架没有苹果Mac电脑的开发者!配合本教程使用,可以快速掌握如何真机测试及上架!
点击苹果证书按钮
点击新增
输入证书密码,名称
这个密码不是账号密码,而是一个保护证书的密码,是p12文件的密码,此密码设置后没有其他地方可以找到,忘记了只能删除证书重新制作,所以请务必记住密码。还有为了安全起见,密码不要太简单。 证书名称是你为了在证书列表里面便于区别的一个字符,自己好辨识就可以,尽量是是字母和数字之类
选择证书类型
带distribution的是发布类型,带development的是开发类型。
apple类型=ios+mac,所以开发时选择ios app development和apple development 类型都是可以的
选择bundle id
只有部分类型的证书需要选择bundle id,例如推送证书。因为大部分证书是不和app关联的。而是通过描述文件profile文件关联app。

如何用苹果app完成ipa安装
当打包完ipa文件后,ipa文件无法直接安装,只能添加udid安装到手机,或者上架才能安装,这里,我分享下使用本站工具上传ipa到app store,无需mac电脑完成ipa文件上架的详细步骤:
1、首先,上架必须要有苹果开发者账号和已经生成了app store类型的打包证书.
2、访问苹果开发者中心,登录进入苹果开发者开发中心: Apple Developer
3、进入控制台后,点击app store connect,进入到app store的管理页面,你是第一次上传,则还没创建app,你需要先创建app。假如不是第一次上传,则无需重新创建app,如下图:
4、已经创建app后,点击app名称就进入填写好各项资料,创建新版本。
5、填写新版本的资料过程中它会要求你选择一个构建版本,但是它提示,这个构建版本要使用旁边的xcode或transport等工具提交,不能在网页上提交。而旁边的工具都需要mac电脑才能安装。这里不用担心,这里我们可以不使用官方提供的工具,因为我们本站提供了上传ipa的工具,工具的地址:www.appuploader.net/
6、登录本站(appuploader)控制台,即可上传ipa,如图:
7、选择文件进行上传,过程中还需要输入苹果开发者中心的专用密码和开发者账号。注意要填写专用密码,不是登录密码。
8、大概过几分钟,就可以上传成功,但要注意,上传成功后苹果开发者中心不能马上见到,因为苹果开发者中心还要验证app的程序是否有其他版本兼容和api过期问题,大概过30分钟左右,就可以在苹果开发者中心的构建版本见到了,选择刚上传的构建版本,你就可以继续在苹果开发者中心继续上架app到app store了。
9、上架的过程中还会要求我们提供各种设备的屏幕快照(截屏),这个比较难搞,因为我们可能没有这么多类型的ios设备怎么截屏。你可以使用本站提供的合成工具自动生成ios截屏。
证书我们这边可以借助辅助工具appuploader
Appuploader可以辅助在Windows、linux或mac系统直接申请iOS证书p12,及上传ipa到App Store,最方便在Windows开发上架没有苹果Mac电脑的开发者!配合本教程使用,可以快速掌握如何真机测试及上架!
点击苹果证书按钮
点击新增
输入证书密码,名称
这个密码不是账号密码,而是一个保护证书的密码,是p12文件的密码,此密码设置后没有其他地方可以找到,忘记了只能删除证书重新制作,所以请务必记住密码。还有为了安全起见,密码不要太简单。 证书名称是你为了在证书列表里面便于区别的一个字符,自己好辨识就可以,尽量是是字母和数字之类
选择证书类型
带distribution的是发布类型,带development的是开发类型。
apple类型=ios+mac,所以开发时选择ios app development和apple development 类型都是可以的
选择bundle id
只有部分类型的证书需要选择bundle id,例如推送证书。因为大部分证书是不和app关联的。而是通过描述文件profile文件关联app。
当打包完ipa文件后,ipa文件无法直接安装,只能添加udid安装到手机,或者上架才能安装,这里,我分享下使用本站工具上传ipa到app store,无需mac电脑完成ipa文件上架的详细步骤:
1、首先,上架必须要有苹果开发者账号和已经生成了app store类型的打包证书.
2、访问苹果开发者中心,登录进入苹果开发者开发中心: Apple Developer
3、进入控制台后,点击app store connect,进入到app store的管理页面,你是第一次上传,则还没创建app,你需要先创建app。假如不是第一次上传,则无需重新创建app,如下图:
4、已经创建app后,点击app名称就进入填写好各项资料,创建新版本。
5、填写新版本的资料过程中它会要求你选择一个构建版本,但是它提示,这个构建版本要使用旁边的xcode或transport等工具提交,不能在网页上提交。而旁边的工具都需要mac电脑才能安装。这里不用担心,这里我们可以不使用官方提供的工具,因为我们本站提供了上传ipa的工具,工具的地址:www.appuploader.net/
6、登录本站(appuploader)控制台,即可上传ipa,如图:
7、选择文件进行上传,过程中还需要输入苹果开发者中心的专用密码和开发者账号。注意要填写专用密码,不是登录密码。
8、大概过几分钟,就可以上传成功,但要注意,上传成功后苹果开发者中心不能马上见到,因为苹果开发者中心还要验证app的程序是否有其他版本兼容和api过期问题,大概过30分钟左右,就可以在苹果开发者中心的构建版本见到了,选择刚上传的构建版本,你就可以继续在苹果开发者中心继续上架app到app store了。
9、上架的过程中还会要求我们提供各种设备的屏幕快照(截屏),这个比较难搞,因为我们可能没有这么多类型的ios设备怎么截屏。你可以使用本站提供的合成工具自动生成ios截屏。
证书我们这边可以借助辅助工具appuploader
Appuploader可以辅助在Windows、linux或mac系统直接申请iOS证书p12,及上传ipa到App Store,最方便在Windows开发上架没有苹果Mac电脑的开发者!配合本教程使用,可以快速掌握如何真机测试及上架!
点击苹果证书按钮
点击新增
输入证书密码,名称
这个密码不是账号密码,而是一个保护证书的密码,是p12文件的密码,此密码设置后没有其他地方可以找到,忘记了只能删除证书重新制作,所以请务必记住密码。还有为了安全起见,密码不要太简单。 证书名称是你为了在证书列表里面便于区别的一个字符,自己好辨识就可以,尽量是是字母和数字之类
选择证书类型
带distribution的是发布类型,带development的是开发类型。
apple类型=ios+mac,所以开发时选择ios app development和apple development 类型都是可以的
选择bundle id
只有部分类型的证书需要选择bundle id,例如推送证书。因为大部分证书是不和app关联的。而是通过描述文件profile文件关联app。

苹果应用上架后多久可以下载
OS上架
iOS APP上架App Store其中一个步骤就是要把ipa文件上传到App Store!
下面进行步骤介绍!
利用Appuploader这个软件,可以在Windows、Linux或Mac系统中申请ios和上传IPA到App Store Connect。
非常的方便,没有Mac也可以用Appuploader在Windows电脑上传ipa到App Store Connect后台。
Appuploader
1、因为苹果开发者账号现在都开通了双重认证,所以需要生成一个上传专用密码才能上传ipa文件。
打开网站
登录苹果开发者账号
登录进去找到安全项目,点击生成专用密码。
密码标签随便输入,123,app等之类的都行。
点击创建生成,(注意密码标签不是专用密码,是由苹果系统生成的一串密码)专用密码就是下面这个样子的密码,把这个密码复制,打开Appuploader
登录Appuploader软件,点击右上角,选择设置,把上传专用密码复制上去,同时勾选上保存密码 ,点击Save保存,然后再回去提交ipa上传就可以正常上传了。
6.1输入专用密码点击OK保存后,点击提交
6.2选择刚生成的ipa包
6.3Appuploader将自动上传你的IPA,包如果很大需要上传一段时间,当出现以下提示时(packages were uploaded successfully 进度条蓝色),说明上传成功。
6.4(这一步一定要认真看下文字说明,很多人这里理不清)
packages were uploaded successfully 进度条蓝色,ipa上传成功后 ,登录iTunes Connect 后台查看上传的ipa
进入APP,点击活动,所有构建版本选项(下图所示) ,这里会显示上传成功的构建版本,如果ipa包没问题刚上传会显示正在处理!
如果你发现没有出现构建版本 ,或者刷新一下构建版本消失了,出现这个问题说明你上传的这个ipa包有问题,苹果会发送具体原因到邮箱(开发者账号就是邮箱地址),登录邮箱查看反馈邮件,修改错误重新打包上传。
修改错误重新打包的时候记得加下版本号,比如你刚上传的是1.0版本,重新打包时增加下版本号如1.1,如果还是跟之前上传过相同的版本号的ipa文件,上传不了。
IOS开发工具官网地址 http://www.applicationloader.net/
最新版本已经优化了没支付688给apple的账号登录流程,无需再安装其他软件。 立即下载最新版本 在appuploader官网首页下载,如果您是windows电脑,则选择点击 windows版,如果是mac 电脑则选择 mac版 下载后解压到电脑中就安装完成了。
windows启动
windows系统里面,解压后双击appuploader.exe就可以直接运行了。 尽量不要放c盘,不要放带中文路径的盘。如果不行可以右击然后选择管理员运行试试。
mac和linux 电脑启动处理
如果是在mac或者linux电脑中,需要使用命令行,对appuploader修改权限。 在appuploader解压后的目录,打开命令行工具,执行下面的命令,使appuploader具备可执行权限。就可以双击启动了。
sudo chmod -R 777 ./*
不会命令行的也可以试试,把各项权限都改查可执行。 mac电脑修改文件权限
部分功能不可用处理
有些电脑可能会对runtime下的组件进行权限控制,导致无法调用组件,例如无法上传,可以双击运行下试试是否被系统权限阻止,正常会是一个黑色框一闪而过,如果系统提示权限错误,则放开权限,允许执行。
驱动安装
安装测试,自动读取设备udid需要安装苹果手机驱动,驱动下载地址。部分电脑缺少程序运行的基本库,可以安装驱动解决。下载后把文件夹内的exe都安装后重启工具。 下载apple手机驱动
如果是未支付688的apple账号,还需要安装icloud icloud下载地址:https://support.apple.com/zh-cn/HT204283 下载icloud
OS上架
iOS APP上架App Store其中一个步骤就是要把ipa文件上传到App Store!
下面进行步骤介绍!
利用Appuploader这个软件,可以在Windows、Linux或Mac系统中申请ios和上传IPA到App Store Connect。
非常的方便,没有Mac也可以用Appuploader在Windows电脑上传ipa到App Store Connect后台。
Appuploader
1、因为苹果开发者账号现在都开通了双重认证,所以需要生成一个上传专用密码才能上传ipa文件。
打开网站
登录苹果开发者账号
登录进去找到安全项目,点击生成专用密码。
密码标签随便输入,123,app等之类的都行。
点击创建生成,(注意密码标签不是专用密码,是由苹果系统生成的一串密码)专用密码就是下面这个样子的密码,把这个密码复制,打开Appuploader
登录Appuploader软件,点击右上角,选择设置,把上传专用密码复制上去,同时勾选上保存密码 ,点击Save保存,然后再回去提交ipa上传就可以正常上传了。
6.1输入专用密码点击OK保存后,点击提交
6.2选择刚生成的ipa包
6.3Appuploader将自动上传你的IPA,包如果很大需要上传一段时间,当出现以下提示时(packages were uploaded successfully 进度条蓝色),说明上传成功。
6.4(这一步一定要认真看下文字说明,很多人这里理不清)
packages were uploaded successfully 进度条蓝色,ipa上传成功后 ,登录iTunes Connect 后台查看上传的ipa
进入APP,点击活动,所有构建版本选项(下图所示) ,这里会显示上传成功的构建版本,如果ipa包没问题刚上传会显示正在处理!
如果你发现没有出现构建版本 ,或者刷新一下构建版本消失了,出现这个问题说明你上传的这个ipa包有问题,苹果会发送具体原因到邮箱(开发者账号就是邮箱地址),登录邮箱查看反馈邮件,修改错误重新打包上传。
修改错误重新打包的时候记得加下版本号,比如你刚上传的是1.0版本,重新打包时增加下版本号如1.1,如果还是跟之前上传过相同的版本号的ipa文件,上传不了。
IOS开发工具官网地址 http://www.applicationloader.net/
最新版本已经优化了没支付688给apple的账号登录流程,无需再安装其他软件。 立即下载最新版本 在appuploader官网首页下载,如果您是windows电脑,则选择点击 windows版,如果是mac 电脑则选择 mac版 下载后解压到电脑中就安装完成了。
windows启动
windows系统里面,解压后双击appuploader.exe就可以直接运行了。 尽量不要放c盘,不要放带中文路径的盘。如果不行可以右击然后选择管理员运行试试。
mac和linux 电脑启动处理
如果是在mac或者linux电脑中,需要使用命令行,对appuploader修改权限。 在appuploader解压后的目录,打开命令行工具,执行下面的命令,使appuploader具备可执行权限。就可以双击启动了。
sudo chmod -R 777 ./*
不会命令行的也可以试试,把各项权限都改查可执行。 mac电脑修改文件权限
部分功能不可用处理
有些电脑可能会对runtime下的组件进行权限控制,导致无法调用组件,例如无法上传,可以双击运行下试试是否被系统权限阻止,正常会是一个黑色框一闪而过,如果系统提示权限错误,则放开权限,允许执行。
驱动安装
安装测试,自动读取设备udid需要安装苹果手机驱动,驱动下载地址。部分电脑缺少程序运行的基本库,可以安装驱动解决。下载后把文件夹内的exe都安装后重启工具。 下载apple手机驱动
如果是未支付688的apple账号,还需要安装icloud icloud下载地址:https://support.apple.com/zh-cn/HT204283 下载icloud

软件开发入门教程网之C++ 信号处理
信号是由操作系统传给进程的中断,会提早终止一个程序。在 UNIX、LINUX、Mac OS X 或 Windows 系统上,可以通过按 Ctrl+C 产生中断。
有些信号不能被程序捕获,但是下表所列信号可以在程序中捕获,并可以基于信号采取适当的动作。这些信号是定义在 C++ 头文件 <csignal> 中。
信号
描述
SIGABRT
程序的异常终止,如调用 abort。
SIGFPE
错误的算术运算,比如除以零或导致溢出的操作。
SIGILL
检测非法指令。
SIGINT
程序终止(interrupt)信号。
SIGSEGV
非法访问内存。
SIGTERM
发送到程序的终止请求。
signal() 函数
C++ 信号处理库提供了 signal 函数,用来捕获突发事件。以下是 signal() 函数的语法:
void (signal (int sig, void (func)(int)))(int);这个看起来有点费劲,以下语法格式更容易理解:
signal(registered signal, signal handler)这个函数接收两个参数:第一个参数是一个整数,代表了信号的编号;第二个参数是一个指向信号处理函数的指针。
让我们编写一个简单的 C++ 程序,使用 signal() 函数捕获 SIGINT 信号。不管您想在程序中捕获什么信号,您都必须使用 signal 函数来注册信号,并将其与信号处理程序相关联。看看下面的实例:
实例
include <iostream>
include <csignal>
include <unistd.h>
using namespace std;
void signalHandler( int signum )
{
cout << "Interrupt signal (" << signum << ") received.\n";
// 清理并关闭
// 终止程序
exit(signum);
}
int main ()
{
// 注册信号 SIGINT 和信号处理程序
signal(SIGINT, signalHandler);
while(1){
cout << "Going to sleep...." << endl;
sleep(1);
}
return 0;
}当上面的代码被编译和执行时,它会产生下列结果:
Going to sleep....
Going to sleep....
Going to sleep....现在,按 Ctrl+C 来中断程序,您会看到程序捕获信号,程序打印如下内容并退出:
Going to sleep....
Going to sleep....
Going to sleep....
Interrupt signal (2) received.raise() 函数
您可以使用函数 raise() 生成信号,该函数带有一个整数信号编号作为参数,语法如下:
int raise (signal sig);在这里,sig 是要发送的信号的编号,这些信号包括:SIGINT、SIGABRT、SIGFPE、SIGILL、SIGSEGV、SIGTERM、SIGHUP。以下是我们使用 raise() 函数内部生成信号的实例:
实例
include <iostream>
include <csignal>
include <unistd.h>
using namespace std;
void signalHandler( int signum )
{
cout << "Interrupt signal (" << signum << ") received.\n";
// 清理并关闭
// 终止程序
exit(signum);
}
int main ()
{
int i = 0;
// 注册信号 SIGINT 和信号处理程序
signal(SIGINT, signalHandler);
while(++i){
cout << "Going to sleep...." << endl;
if( i == 3 ){
raise( SIGINT);
}
sleep(1);
}
return 0;
}当上面的代码被编译和执行时,它会产生下列结果,并会自动退出:
Going to sleep....
Going to sleep....
Going to sleep....
Interrupt signal (2) received.
信号是由操作系统传给进程的中断,会提早终止一个程序。在 UNIX、LINUX、Mac OS X 或 Windows 系统上,可以通过按 Ctrl+C 产生中断。
有些信号不能被程序捕获,但是下表所列信号可以在程序中捕获,并可以基于信号采取适当的动作。这些信号是定义在 C++ 头文件 <csignal> 中。
信号
描述
SIGABRT
程序的异常终止,如调用 abort。
SIGFPE
错误的算术运算,比如除以零或导致溢出的操作。
SIGILL
检测非法指令。
SIGINT
程序终止(interrupt)信号。
SIGSEGV
非法访问内存。
SIGTERM
发送到程序的终止请求。
signal() 函数
C++ 信号处理库提供了 signal 函数,用来捕获突发事件。以下是 signal() 函数的语法:
void (signal (int sig, void (func)(int)))(int);这个看起来有点费劲,以下语法格式更容易理解:
signal(registered signal, signal handler)这个函数接收两个参数:第一个参数是一个整数,代表了信号的编号;第二个参数是一个指向信号处理函数的指针。
让我们编写一个简单的 C++ 程序,使用 signal() 函数捕获 SIGINT 信号。不管您想在程序中捕获什么信号,您都必须使用 signal 函数来注册信号,并将其与信号处理程序相关联。看看下面的实例:
实例
include <iostream>
include <csignal>
include <unistd.h>
using namespace std;
void signalHandler( int signum )
{
cout << "Interrupt signal (" << signum << ") received.\n";
// 清理并关闭
// 终止程序
exit(signum);
}
int main ()
{
// 注册信号 SIGINT 和信号处理程序
signal(SIGINT, signalHandler);
while(1){
cout << "Going to sleep...." << endl;
sleep(1);
}
return 0;
}当上面的代码被编译和执行时,它会产生下列结果:
Going to sleep....
Going to sleep....
Going to sleep....现在,按 Ctrl+C 来中断程序,您会看到程序捕获信号,程序打印如下内容并退出:
Going to sleep....
Going to sleep....
Going to sleep....
Interrupt signal (2) received.raise() 函数
您可以使用函数 raise() 生成信号,该函数带有一个整数信号编号作为参数,语法如下:
int raise (signal sig);在这里,sig 是要发送的信号的编号,这些信号包括:SIGINT、SIGABRT、SIGFPE、SIGILL、SIGSEGV、SIGTERM、SIGHUP。以下是我们使用 raise() 函数内部生成信号的实例:
实例
include <iostream>
include <csignal>
include <unistd.h>
using namespace std;
void signalHandler( int signum )
{
cout << "Interrupt signal (" << signum << ") received.\n";
// 清理并关闭
// 终止程序
exit(signum);
}
int main ()
{
int i = 0;
// 注册信号 SIGINT 和信号处理程序
signal(SIGINT, signalHandler);
while(++i){
cout << "Going to sleep...." << endl;
if( i == 3 ){
raise( SIGINT);
}
sleep(1);
}
return 0;
}当上面的代码被编译和执行时,它会产生下列结果,并会自动退出:
Going to sleep....
Going to sleep....
Going to sleep....
Interrupt signal (2) received.

软件开发入门教程网之C++ 标准库
C++ 标准库可以分为两部分:
• 标准函数库: 这个库是由通用的、独立的、不属于任何类的函数组成的。函数库继承自 C 语言。
• 面向对象类库: 这个库是类及其相关函数的集合。
C++ 标准库包含了所有的 C 标准库,为了支持类型安全,做了一定的添加和修改。
标准函数库
标准函数库分为以下几类:
• 输入/输出 I/O
• 字符串和字符处理
• 数学
• 时间、日期和本地化
• 动态分配
• 其他
• 宽字符函数
面向对象类库
标准的 C++ 面向对象类库定义了大量支持一些常见操作的类,比如输入/输出 I/O、字符串处理、数值处理。面向对象类库包含以下内容:
• 标准的 C++ I/O 类
• String 类
• 数值类
• STL 容器类
• STL 算法
• STL 函数对象
• STL 迭代器
• STL 分配器
• 本地化库
• 异常处理类
• 杂项支持库
C++ 标准库可以分为两部分:
• 标准函数库: 这个库是由通用的、独立的、不属于任何类的函数组成的。函数库继承自 C 语言。
• 面向对象类库: 这个库是类及其相关函数的集合。
C++ 标准库包含了所有的 C 标准库,为了支持类型安全,做了一定的添加和修改。
标准函数库
标准函数库分为以下几类:
• 输入/输出 I/O
• 字符串和字符处理
• 数学
• 时间、日期和本地化
• 动态分配
• 其他
• 宽字符函数
面向对象类库
标准的 C++ 面向对象类库定义了大量支持一些常见操作的类,比如输入/输出 I/O、字符串处理、数值处理。面向对象类库包含以下内容:
• 标准的 C++ I/O 类
• String 类
• 数值类
• STL 容器类
• STL 算法
• STL 函数对象
• STL 迭代器
• STL 分配器
• 本地化库
• 异常处理类
• 杂项支持库

http://kxdang.com/topic/cplusplus/cpp-signal-handling.html
软件开发入门教程网之C++ 信号处理
信号是由操作系统传给进程的中断,会提早终止一个程序。在 UNIX、LINUX、Mac OS X 或 Windows 系统上,可以通过按 Ctrl+C 产生中断。
有些信号不能被程序捕获,但是下表所列信号可以在程序中捕获,并可以基于信号采取适当的动作。这些信号是定义在 C++ 头文件 <csignal> 中。
信号
描述
SIGABRT
程序的异常终止,如调用 abort。
SIGFPE
错误的算术运算,比如除以零或导致溢出的操作。
SIGILL
检测非法指令。
SIGINT
程序终止(interrupt)信号。
SIGSEGV
非法访问内存。
SIGTERM
发送到程序的终止请求。
signal() 函数
C++ 信号处理库提供了 signal 函数,用来捕获突发事件。以下是 signal() 函数的语法:
void (signal (int sig, void (func)(int)))(int);这个看起来有点费劲,以下语法格式更容易理解:
signal(registered signal, signal handler)这个函数接收两个参数:第一个参数是一个整数,代表了信号的编号;第二个参数是一个指向信号处理函数的指针。
让我们编写一个简单的 C++ 程序,使用 signal() 函数捕获 SIGINT 信号。不管您想在程序中捕获什么信号,您都必须使用 signal 函数来注册信号,并将其与信号处理程序相关联。看看下面的实例:
实例
include <iostream>
include <csignal>
include <unistd.h>
using namespace std;
void signalHandler( int signum )
{
cout << "Interrupt signal (" << signum << ") received.\n";
// 清理并关闭
// 终止程序
exit(signum);
}
int main ()
{
// 注册信号 SIGINT 和信号处理程序
signal(SIGINT, signalHandler);
while(1){
cout << "Going to sleep...." << endl;
sleep(1);
}
return 0;
}当上面的代码被编译和执行时,它会产生下列结果:
Going to sleep....
Going to sleep....
Going to sleep....现在,按 Ctrl+C 来中断程序,您会看到程序捕获信号,程序打印如下内容并退出:
Going to sleep....
Going to sleep....
Going to sleep....
Interrupt signal (2) received.raise() 函数
您可以使用函数 raise() 生成信号,该函数带有一个整数信号编号作为参数,语法如下:
int raise (signal sig);在这里,sig 是要发送的信号的编号,这些信号包括:SIGINT、SIGABRT、SIGFPE、SIGILL、SIGSEGV、SIGTERM、SIGHUP。以下是我们使用 raise() 函数内部生成信号的实例:
实例
include <iostream>
include <csignal>
include <unistd.h>
using namespace std;
void signalHandler( int signum )
{
cout << "Interrupt signal (" << signum << ") received.\n";
// 清理并关闭
// 终止程序
exit(signum);
}
int main ()
{
int i = 0;
// 注册信号 SIGINT 和信号处理程序
signal(SIGINT, signalHandler);
while(++i){
cout << "Going to sleep...." << endl;
if( i == 3 ){
raise( SIGINT);
}
sleep(1);
}
return 0;
}当上面的代码被编译和执行时,它会产生下列结果,并会自动退出:
Going to sleep....
Going to sleep....
Going to sleep....
Interrupt signal (2) received.
软件开发入门教程网之C++ 信号处理
信号是由操作系统传给进程的中断,会提早终止一个程序。在 UNIX、LINUX、Mac OS X 或 Windows 系统上,可以通过按 Ctrl+C 产生中断。
有些信号不能被程序捕获,但是下表所列信号可以在程序中捕获,并可以基于信号采取适当的动作。这些信号是定义在 C++ 头文件 <csignal> 中。
信号
描述
SIGABRT
程序的异常终止,如调用 abort。
SIGFPE
错误的算术运算,比如除以零或导致溢出的操作。
SIGILL
检测非法指令。
SIGINT
程序终止(interrupt)信号。
SIGSEGV
非法访问内存。
SIGTERM
发送到程序的终止请求。
signal() 函数
C++ 信号处理库提供了 signal 函数,用来捕获突发事件。以下是 signal() 函数的语法:
void (signal (int sig, void (func)(int)))(int);这个看起来有点费劲,以下语法格式更容易理解:
signal(registered signal, signal handler)这个函数接收两个参数:第一个参数是一个整数,代表了信号的编号;第二个参数是一个指向信号处理函数的指针。
让我们编写一个简单的 C++ 程序,使用 signal() 函数捕获 SIGINT 信号。不管您想在程序中捕获什么信号,您都必须使用 signal 函数来注册信号,并将其与信号处理程序相关联。看看下面的实例:
实例
include <iostream>
include <csignal>
include <unistd.h>
using namespace std;
void signalHandler( int signum )
{
cout << "Interrupt signal (" << signum << ") received.\n";
// 清理并关闭
// 终止程序
exit(signum);
}
int main ()
{
// 注册信号 SIGINT 和信号处理程序
signal(SIGINT, signalHandler);
while(1){
cout << "Going to sleep...." << endl;
sleep(1);
}
return 0;
}当上面的代码被编译和执行时,它会产生下列结果:
Going to sleep....
Going to sleep....
Going to sleep....现在,按 Ctrl+C 来中断程序,您会看到程序捕获信号,程序打印如下内容并退出:
Going to sleep....
Going to sleep....
Going to sleep....
Interrupt signal (2) received.raise() 函数
您可以使用函数 raise() 生成信号,该函数带有一个整数信号编号作为参数,语法如下:
int raise (signal sig);在这里,sig 是要发送的信号的编号,这些信号包括:SIGINT、SIGABRT、SIGFPE、SIGILL、SIGSEGV、SIGTERM、SIGHUP。以下是我们使用 raise() 函数内部生成信号的实例:
实例
include <iostream>
include <csignal>
include <unistd.h>
using namespace std;
void signalHandler( int signum )
{
cout << "Interrupt signal (" << signum << ") received.\n";
// 清理并关闭
// 终止程序
exit(signum);
}
int main ()
{
int i = 0;
// 注册信号 SIGINT 和信号处理程序
signal(SIGINT, signalHandler);
while(++i){
cout << "Going to sleep...." << endl;
if( i == 3 ){
raise( SIGINT);
}
sleep(1);
}
return 0;
}当上面的代码被编译和执行时,它会产生下列结果,并会自动退出:
Going to sleep....
Going to sleep....
Going to sleep....
Interrupt signal (2) received.

软件开发入门教程网之C++ 引用
引用变量是一个别名,也就是说,它是某个已存在变量的另一个名字。一旦把引用初始化为某个变量,就可以使用该引用名称或变量名称来指向变量。
C++ 引用 vs 指针
引用很容易与指针混淆,它们之间有三个主要的不同:
• 不存在空引用。引用必须连接到一块合法的内存。
• 一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。
• 引用必须在创建时被初始化。指针可以在任何时间被初始化。
C++ 中创建引用
试想变量名称是变量附属在内存位置中的标签,您可以把引用当成是变量附属在内存位置中的第二个标签。因此,您可以通过原始变量名称或引用来访问变量的内容。例如:
int i = 17;我们可以为 i 声明引用变量,如下所示:
int& r = i;
double& s = d;在这些声明中,& 读作引用。因此,第一个声明可以读作 "r 是一个初始化为 i 的整型引用",第二个声明可以读作 "s 是一个初始化为 d 的 double 型引用"。下面的实例使用了 int 和 double 引用:
实例
include <iostream>
using namespace std;
int main ()
{
// 声明简单的变量
int i;
double d;
// 声明引用变量
int& r = i;
double& s = d;
i = 5;
cout << "Value of i : " << i << endl;
cout << "Value of i reference : " << r << endl;
d = 11.7;
cout << "Value of d : " << d << endl;
cout << "Value of d reference : " << s << endl;
return 0;
}当上面的代码被编译和执行时,它会产生下列结果:
Value of i : 5
Value of i reference : 5
Value of d : 11.7
Value of d reference : 11.7引用通常用于函数参数列表和函数返回值。下面列出了 C++ 程序员必须清楚的两个与 C++ 引用相关的重要概念:
概念
描述
把引用作为参数
C++ 支持把引用作为参数传给函数,这比传一般的参数更安全。
把引用作为返回值
可以从 C++ 函数中返回引用,就像返回其他数据类型一样。
引用变量是一个别名,也就是说,它是某个已存在变量的另一个名字。一旦把引用初始化为某个变量,就可以使用该引用名称或变量名称来指向变量。
C++ 引用 vs 指针
引用很容易与指针混淆,它们之间有三个主要的不同:
• 不存在空引用。引用必须连接到一块合法的内存。
• 一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。
• 引用必须在创建时被初始化。指针可以在任何时间被初始化。
C++ 中创建引用
试想变量名称是变量附属在内存位置中的标签,您可以把引用当成是变量附属在内存位置中的第二个标签。因此,您可以通过原始变量名称或引用来访问变量的内容。例如:
int i = 17;我们可以为 i 声明引用变量,如下所示:
int& r = i;
double& s = d;在这些声明中,& 读作引用。因此,第一个声明可以读作 "r 是一个初始化为 i 的整型引用",第二个声明可以读作 "s 是一个初始化为 d 的 double 型引用"。下面的实例使用了 int 和 double 引用:
实例
include <iostream>
using namespace std;
int main ()
{
// 声明简单的变量
int i;
double d;
// 声明引用变量
int& r = i;
double& s = d;
i = 5;
cout << "Value of i : " << i << endl;
cout << "Value of i reference : " << r << endl;
d = 11.7;
cout << "Value of d : " << d << endl;
cout << "Value of d reference : " << s << endl;
return 0;
}当上面的代码被编译和执行时,它会产生下列结果:
Value of i : 5
Value of i reference : 5
Value of d : 11.7
Value of d reference : 11.7引用通常用于函数参数列表和函数返回值。下面列出了 C++ 程序员必须清楚的两个与 C++ 引用相关的重要概念:
概念
描述
把引用作为参数
C++ 支持把引用作为参数传给函数,这比传一般的参数更安全。
把引用作为返回值
可以从 C++ 函数中返回引用,就像返回其他数据类型一样。