一般都是在网页上写一段javascript脚本,校验上传文件的后缀名,有白名单形式也有黑名单形式。
查看源代码可以看到有如下代码对上传文件类型进行了限制:
<script type="text/javascript"> function checkFile() {
var file=document.getElementsByName('upload_file')[0].value;
if (file==null || file=="") {
alert("请选择要上传的文件!");
return false;
}
//定义允许上传的文件类型
var allow_ext=".jpg|.png|.gif";
//提取上传文件的类型
var ext_name=file.substring(file.lastIndexOf("."));
//判断上传文件类型是否允许上传
if (allow_ext.indexOf(ext_name)==-1) {
var errMsg="该文件不允许上传,请上传" + allow_ext + "类型的文件,当前文件类型为:" + ext_name;
alert(errMsg);
return false;
}
} </script>
我们可以看到对上传文件类型进行了限制。
或者可以不加载所有js,还可以将html源码copy一份到本地,然后对相应代码进行修改,本地提交即可。
即可上传成功:
if (file_exists(UPLOAD_PATH)) {
$deny_ext=array('.asp','.aspx','.php','.jsp');
$file_name=trim($_FILES['upload_file']['name']);
$file_name=deldot($file_name);//删除文件名末尾的点
$file_ext=strrchr($file_name, '.');
$file_ext=strtolower($file_ext); //转换为小写
$file_ext=str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext=trim($file_ext); //收尾去空
if(!in_array($file_ext, $deny_ext)) {
$temp_file=$_FILES['upload_file']['tmp_name'];
$img_path=UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;
if (move_uploaded_file($temp_file,$img_path)) {
$is_upload=true;
} else {
$msg='上传出错!';
}
} else {
$msg='不允许上传.asp,.aspx,.php,.jsp后缀文件!';
}
} else {
$msg=UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
这里做了黑名单处理,我们可以通过特殊可解析后缀进行绕过。
之前在https://www.jianshu.com/p/1ccbab572974中总结过,这里不再赘述,可以使用php3,phtml等绕过。
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext=array(".php",".php5",".php4",".php3",".php2","php1",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2","pHp1",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf");
$file_name=trim($_FILES['upload_file']['name']);
$file_name=deldot($file_name);//删除文件名末尾的点
$file_ext=strrchr($file_name, '.');
$file_ext=strtolower($file_ext); //转换为小写
$file_ext=str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext=trim($file_ext); //收尾去空
if (!in_array($file_ext, $deny_ext)) {
$temp_file=$_FILES['upload_file']['tmp_name'];
$img_path=UPLOAD_PATH.'/'.$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload=true;
} else {
$msg='上传出错!';
}
} else {
$msg='此文件不允许上传!';
}
} else {
$msg=UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
?>
我们发现黑名单限制了很多后缀名,但是没有限制.htaccess
.htaccess文件是Apache服务器中的一个配置文件,它负责相关目录下的网页配置.通过htaccess文件,可以实现:网页301重定向、自定义404页面、改变文件扩展名、允许/阻止特定的用户或者目录的访问、禁止目录列表、配置默认文档等功能。
我们需要上传一个.htaccess文件,内容为:
SetHandler application/x-httpd-php
这样所有的文件都会解析为php,接下来上传图片马即可
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext=array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");
$file_name=trim($_FILES['upload_file']['name']);
$file_name=deldot($file_name);//删除文件名末尾的点
$file_ext=strrchr($file_name, '.');
$file_ext=str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext=trim($file_ext); //首尾去空
if (!in_array($file_ext, $deny_ext)) {
$temp_file=$_FILES['upload_file']['tmp_name'];
$img_path=UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload=true;
} else {
$msg='上传出错!';
}
} else {
$msg='此文件类型不允许上传!';
}
} else {
$msg=UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
我们发现对.htaccess也进行了检测,但是没有对大小写进行统一。
后缀名改为PHP即可
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext=array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");
$file_name=$_FILES['upload_file']['name'];
$file_name=deldot($file_name);//删除文件名末尾的点
$file_ext=strrchr($file_name, '.');
$file_ext=strtolower($file_ext); //转换为小写
$file_ext=str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
if (!in_array($file_ext, $deny_ext)) {
$temp_file=$_FILES['upload_file']['tmp_name'];
$img_path=UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;
if (move_uploaded_file($temp_file,$img_path)) {
$is_upload=true;
} else {
$msg='上传出错!';
}
} else {
$msg='此文件不允许上传';
}
} else {
$msg=UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
黑名单没有对文件中的空格进行处理,可在后缀名中加空格绕过。
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext=array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");
$file_name=trim($_FILES['upload_file']['name']);
$file_ext=strrchr($file_name, '.');
$file_ext=strtolower($file_ext); //转换为小写
$file_ext=str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext=trim($file_ext); //首尾去空
if (!in_array($file_ext, $deny_ext)) {
$temp_file=$_FILES['upload_file']['tmp_name'];
$img_path=UPLOAD_PATH.'/'.$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload=true;
} else {
$msg='上传出错!';
}
} else {
$msg='此文件类型不允许上传!';
}
} else {
$msg=UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
windows会对文件中的点进行自动去除,所以可以在文件末尾加点绕过,不再赘述
同windows特性,可在后缀名中加” ::$DATA”绕过,不再赘述
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext=array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");
$file_name=trim($_FILES['upload_file']['name']);
$file_name=deldot($file_name);//删除文件名末尾的点
$file_ext=strrchr($file_name, '.');
$file_ext=strtolower($file_ext); //转换为小写
$file_ext=str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext=trim($file_ext); //首尾去空
if (!in_array($file_ext, $deny_ext)) {
$temp_file=$_FILES['upload_file']['tmp_name'];
$img_path=UPLOAD_PATH.'/'.$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload=true;
} else {
$msg='上传出错!';
}
} else {
$msg='此文件类型不允许上传!';
}
} else {
$msg=UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
这里对文件名进行了处理,删除了文件名末尾的点,并且把处理过的文件名拼接到路径中。
这里我们可以构造文件名1.PHP. . (点+空格+点),经过处理后,文件名变成1.PHP.,即可绕过。
if (file_exists(UPLOAD_PATH)) {
$deny_ext=array("php","php5","php4","php3","php2","html","htm","phtml","pht","jsp","jspa","jspx","jsw","jsv","jspf","jtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","swf","htaccess");
$file_name=trim($_FILES['upload_file']['name']);
$file_name=str_ireplace($deny_ext,"", $file_name);
$temp_file=$_FILES['upload_file']['tmp_name'];
$img_path=UPLOAD_PATH.'/'.$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload=true;
} else {
$msg='上传出错!';
}
} else {
$msg=UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
这里我们可以看到将文件名替换为空,我们可以采用双写绕过:1.pphphp
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
if (($_FILES['upload_file']['type']=='image/jpeg') || ($_FILES['upload_file']['type']=='image/png') || ($_FILES['upload_file']['type']=='image/gif')) {
$temp_file=$_FILES['upload_file']['tmp_name'];
$img_path=UPLOAD_PATH . '/' . $_FILES['upload_file']['name'];
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload=true;
} else {
$msg='上传出错!';
}
} else {
$msg='文件类型不正确,请重新上传!';
}
} else {
$msg=UPLOAD_PATH.'文件夹不存在,请手工创建!';
}
这里检查Content-type,我们burp抓包修改即可绕过:
if(isset($_POST['submit'])){
$ext_arr=array('jpg','png','gif');
$file_ext=substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1);
if(in_array($file_ext,$ext_arr)){
$temp_file=$_FILES['upload_file']['tmp_name'];
$img_path=$_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;
if(move_uploaded_file($temp_file,$img_path)){
$is_upload=true;
} else {
$msg='上传出错!';
}
} else{
$msg="只允许上传.jpg|.png|.gif类型文件!";
}
}
$img_path直接拼接,因此可以利用%00截断绕过
然后直接访问/upload/1.php即可
if(isset($_POST['submit'])){
$ext_arr=array('jpg','png','gif');
$file_ext=substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1);
if(in_array($file_ext,$ext_arr)){
$temp_file=$_FILES['upload_file']['tmp_name'];
$img_path=$_POST['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;
if(move_uploaded_file($temp_file,$img_path)){
$is_upload=true;
} else {
$msg="上传失败";
}
} else {
$msg="只允许上传.jpg|.png|.gif类型文件!";
}
}
?>
save_path是通过post传进来的,还是利用00截断,但这次需要在二进制中进行修改,因为post不会像get对%00进行自动解码。
接下来访问1.php即可
主要是检测文件内容开始处的文件幻数,比如图片类型的文件幻数如下,
要绕过jpg 文件幻数检测就要在文件开头写上下图的值:
Value=FF D8 FF E0 00 10 4A 46 49 46
要绕过gif 文件幻数检测就要在文件开头写上下图的值
Value=47 49 46 38 39 61
要绕过png 文件幻数检测就要在文件开头写上下面的值
Value=89 50 4E 47
然后在文件幻数后面加上自己的一句话木马代码就行了
图像文件相关信息检测常用的就是getimagesize()函数
只需要把文件头部分伪造好就ok 了,就是在幻数的基础上还加了一些文件信息
有点像下面的结构
GIF89a
(...some binary data for image...)
<?php phpinfo(); ?>
(... skipping the rest of binary data ...)
本次环境中的文件头检测,getimagesize,php_exif都可以用图片马绕过:
copy normal.jpg /b + shell.php /a webshell.jpg
一般是调用API 或函数去进行文件加载测试,常见的是图像渲染测试,甚至是进行二次渲染(过滤效果几乎最强)。对渲染/加载测试的攻击方式是代码注入绕过,对二次渲染的攻击方式是攻击文件加载器自身。
可以用图像处理软件对一张图片进行代码注入
用winhex 看数据可以分析出这类工具的原理是
在不破坏文件本身的渲染情况下找一个空白区进行填充代码,一般会是图片的注释区
对于渲染测试基本上都能绕过,毕竟本身的文件结构是完整的
imagecreatefromjpeg二次渲染它相当于是把原本属于图像数据的部分抓了出来,再用自己的API 或函数进行重新渲染在这个过程中非图像数据的部分直接就隔离开了
if (isset($_POST['submit'])){
// 获得上传文件的基本信息,文件名,类型,大小,临时文件路径
$filename=$_FILES['upload_file']['name'];
$filetype=$_FILES['upload_file']['type'];
$tmpname=$_FILES['upload_file']['tmp_name'];
$target_path=UPLOAD_PATH.basename($filename);
// 获得上传文件的扩展名
$fileext=substr(strrchr($filename,"."),1);
//判断文件后缀与类型,合法才进行上传操作
if(($fileext=="jpg") && ($filetype=="image/jpeg")){
if(move_uploaded_file($tmpname,$target_path))
{
//使用上传的图片生成新的图片
$im=imagecreatefromjpeg($target_path);
if($im==false){
$msg="该文件不是jpg格式的图片!";
@unlink($target_path);
}else{
//给新图片指定文件名
srand(time());
$newfilename=strval(rand()).".jpg";
$newimagepath=UPLOAD_PATH.$newfilename;
imagejpeg($im,$newimagepath);
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path=UPLOAD_PATH.$newfilename;
@unlink($target_path);
$is_upload=true;
}
} else {
$msg="上传出错!";
}
}else if(($fileext=="png") && ($filetype=="image/png")){
if(move_uploaded_file($tmpname,$target_path))
{
//使用上传的图片生成新的图片
$im=imagecreatefrompng($target_path);
if($im==false){
$msg="该文件不是png格式的图片!";
@unlink($target_path);
}else{
//给新图片指定文件名
srand(time());
$newfilename=strval(rand()).".png";
$newimagepath=UPLOAD_PATH.$newfilename;
imagepng($im,$newimagepath);
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path=UPLOAD_PATH.$newfilename;
@unlink($target_path);
$is_upload=true;
}
} else {
$msg="上传出错!";
}
}else if(($fileext=="gif") && ($filetype=="image/gif")){
if(move_uploaded_file($tmpname,$target_path))
{
//使用上传的图片生成新的图片
$im=imagecreatefromgif($target_path);
if($im==false){
$msg="该文件不是gif格式的图片!";
@unlink($target_path);
}else{
//给新图片指定文件名
srand(time());
$newfilename=strval(rand()).".gif";
$newimagepath=UPLOAD_PATH.$newfilename;
imagegif($im,$newimagepath);
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path=UPLOAD_PATH.$newfilename;
@unlink($target_path);
$is_upload=true;
}
} else {
$msg="上传出错!";
}
}else{
$msg="只允许上传后缀为.jpg|.png|.gif的图片文件!";
}
}
本关综合判断了后缀名、content-type,以及利用imagecreatefromgif判断是否为gif图片,最后再做了一次二次渲染。
得去找图片经过GD库转化后没有改变的部分,再将未改变的部分修改为相应的php代码。
if(isset($_POST['submit'])){
$ext_arr=array('jpg','png','gif');
$file_name=$_FILES['upload_file']['name'];
$temp_file=$_FILES['upload_file']['tmp_name'];
$file_ext=substr($file_name,strrpos($file_name,".")+1);
$upload_file=UPLOAD_PATH . '/' . $file_name;
if(move_uploaded_file($temp_file, $upload_file)){
if(in_array($file_ext,$ext_arr)){
$img_path=UPLOAD_PATH . '/'. rand(10, 99).date("YmdHis").".".$file_ext;
rename($upload_file, $img_path);
$is_upload=true;
}else{
$msg="只允许上传.jpg|.png|.gif类型文件!";
unlink($upload_file);
}
}else{
$msg='上传出错!';
}
}
这里先将文件上传到服务器,然后通过rename修改名称,再通过unlink删除文件,因此可以通过条件竞争的方式在unlink之前,访问webshell。
然后不断访问webshell:
上传成功。
参考链接:
2人点赞
知识归纳
读
introduction
Swift 是一种适用于iOS/macOS应用开发、服务器端的编程语言。自2014年苹果发布 Swift 语言以来,Swift5 实现了 ABI 稳定性、Module 稳定性和Library Evolution,与Objective-C(下文简称“OC”)相比,Swift 在开发效率、安全、编译优化、运行性能和内存管理方面具有显著优势。(官方博客:https://www.swift.org/about/)
百度App 已在工程和环境上支持 Swift 开发,百度搜索大前端团队负责搜索服务的稳定落地,我们积极探索 Swift的应用,希望能大幅提升开发效率和灵活性、提升端用户的搜索体验。然而,在实施过程中可能会遇到各种问题,例如代码陈旧且不支持Swift,人员对Swift掌握不够熟练、意识不足,协作方对Swift的支持不足等。
对于其他语言来说,Swift相对年轻,我们在实践过程中整理一些常见问题及其解决方法,希望能帮助读者更顺利地使用Swift进行编程,提高研发效率。
全文6947字,预计阅读时间18分钟。
GEEK TALK
01
Swift 适用场景
在决定是否引入Swift前,我们需要判断场景是否适合。通常情况下,可以用OC的场景均适合使用Swift,但也有一些不太适合直接替换的场景,需要慎重,比如:
1、涉及OC动态性,频繁在runtime时操作属性和方法;
2、核心基础功能,出现问题影响面较大的逻辑;
3、调用C++(目前Swift不能直接调用C++);
4、继承不支持Swift组件的类。
此外,对使用OC比较久远的工程,使用Swift前也应注意:
1、能在工程环境和单独模块上支持Swift;
2、模块较多的工程,可以内外OC和Swift混编;
3、为了避免Swift Waring带来的潜在问题,可以把SWIFT_TREAT_WARNINGS_AS_ERRORS设置为YES,这样警告会作为错误,辅助程序员更好的规范代码;
4、模块Module化后,要注意维护 umbrella header 中的公开头文件。
GEEK TALK
02
Swift的基本用法
2.1 Swift 的字符串为什么这么难用?
如:字符串不能通过索引取字符
2.2 try try? try! 的区别
当你进行文件操作时,可能会遇到需要使用try、try?和try!的情况。它们在异常处理方面有所不同。
1、使用try时,如果出现异常,程序会进入异常处理流程,你可以在catch语句块中处理这个异常。
2、使用try?时,如果发生异常,它不会进入异常处理流程,而是返回一个可选值类型。也就是说,如果出现异常,它将返回nil。
3、使用try!时,它不允许异常继续传播。一旦出现异常,程序会立即停止执行。
因此,在文件操作中,你可以根据需要选择合适的异常处理方式。在百度App中一般推荐使用try?。
2.3 public 和 open 的区别
在Swift语言中,public和open都是用于在模块中声明需要对外界暴露的函数的关键字,但它们在继承和公开程度上有所不同。
1、public关键字修饰的类在模块外部无法被继承。这意味着,如果其他模块试图继承这个类,编译器会报错。这样的限制可以保护类的完整性,但也可能限制了其在其他模块中的可重用性。
2、open关键字则允许任意继承。如果一个类被open关键字修饰,那么其他模块中的类可以自由地继承这个类,不受任何限制。这样的公开程度使得open关键字修饰的类在模块间的重用性和扩展性更加灵活。
从公开程度上来说,public的限制比open更严格,所以可以说public < open,即public的公开程度比open要低。
2.4 解析JSON情况
在Swift中解析JSON的情况,如果自行将JSON转换为字典,需要涉及到类型判断、转换等操作,代码比较复杂。这时可以使用第三方库SwiftyJSON、ObjectMapper或者系统库JSONEncoder来简化操作,提高开发效率。
2.5 UIView子类必须添加init?(coder decoder: NSCoder)的原因
1、这是NSCoding protocol定义的,遵守了NSCoding protocol的所有类必须继承。只是有的情况会隐式继承,而有的情况下需要显示实现。
2、当我们在子类定义了指定初始化器(包括自定义和重写父类指定初始化器),那么必须显示实现required init?(coder aDecoder: NSCoder),而其他情况下则会隐式继承,我们可以不用理会。
3、当我们使用storyboard实现界面的时候,程序会调用这个初始化器。
4、注意要去掉fatalError,fatalError的意思是无条件停止执行并打印。
2.6 Swift类和子类的初始化
Swift的类和子类初始化涉及到两个关键阶段。首先,确保所有的存储属性被赋予初始值,然后,在实例准备使用之前,可以自定义存储属性的值。为了确保这两个阶段成功,实施了四步安全检查,详细如下:
1、在完成本类所有存储属性赋值之后,指定构造器才能向上代理到父类的构造器。
2、在为继承的属性设置新值之前,指定构造器必须向上代理调用父类构造器。
3、便利构造器必须先调用其他构造器,再为任意属性(包括所有同类中定义的)赋新值。
4、在第一阶段构造完成之前,构造器不能调用任何实例方法,不能读取任何实例属性的值,不能引用self作为一个值。
总之,类初始化必须完成的一个任务就是让所有的存储属性都有初始值(optional 除外)。如果父类有指定初始化,子类必须也有指定初始化,并且必须调用父类的其中一个指定初始化(如果是必须初始化,就是重载),并遵循两段式初始化的规则。一个便利初始化必须调用同一类中的初始化方法(可以是另一个便利初始化,也可以是指定初始化),但最终一定会调用到一个指定初始化。便利初始化不遵循两段式初始化的规则,不能被子类调用或者重载。
GEEK TALK
03
OC与Swift的互相调用及跳转
3.1 组件内Swift文件调用公开OC头文件
3.2 组件内Swift文件调用非公开(私有)的OC文件
组件应该尽可能少的公开暴露头文件,但Swift和OC混编不可避免使用OC非公开头文件,因此我们可以采取以下措施:将Framework 中将私有头文件声明为一个私有 module(modulemap内声明),由组件内的 Swift 源码 import 该私有 module 即可。
1、创建Private.modulemap文件,以NewModule做为组件名为例,可以命名为NewModule.private.modulemap,内容为下,module后面加_Private
framework module NewModule_Private {
header "xxxxx.h"
}
framework module NewModule_Private {
umbrella header "NewModule_Private.h"
export *
module * { export * }
}
2、在组件build settings中配置MODULEMAP_PRIVATE_FILE路径,MODULEMAP_PRIVATE_FILE='NewModule.private.modulemap';百度App中在NewModule.boxspec中如下代码设置路径;
s.xcconfig={
'MODULEMAP_PRIVATE_FILE'=> '${BOX_ROOT}/NewModule.private.modulemap'
}
3、将NewModule.private.modulemap添加到工程目录;百度App中在NewModule.boxspec中如下代码设置路径;
s.refer_files=[
"NewModule.private.modulemap",
]
4、将xxxxx.h设置为Private header,百度App中在NewModule.boxspec中如下代码设置xxxxx.h到Private header
s.private_headers=[
"Sources/xxxxx.h"
]
5、调用方式
import NewModule_Private
let objectX=xxxxx()
print(objectX)
注意:
3.3 组件内OC文件如何调用Swift文件?
3.4 OC中的向前声明,被Swift文件引用该组件会报错
如error: cannot find protocol definition for 'xxxProtocol'
1、暂时设置 SWIFT_TREAT_WARNINGS_AS_ERRORS 为 NO
2、import xxxProtocol 不要向前声明
3、使用 pragma 忽略警告
3.8 Swift怎么用OC定义的宏?
下面这种定义为常量的宏可以使用
#define APP_LANGUAGE_EN @"en"
#define kNavigationBarHeight 44.0
下面带有方法调用的宏不可以使用
#define kScreenHeight [[UIScreen mainScreen] bounds].size.height
#define kScreenWidth [[UIScreen mainScreen] bounds].size.width
下面带有静态常量swift不能使用,可以改成宏
static NSString *const StopTabRefreshNotifyNameHtml=@"TabRefreshNotifyNameHtml";
3.6 Swift与OC泛型的混编
@interface BBAXYZ<T> : NSObject <BBAXYZEventProtocol>
@property (nonatomic, weak) T page;
@end
这个泛型的使用导致无法使用Swift来继承和开发BBAXYZ的子类。然而,这个基础框架是业务的核心部分,因此,我们需要在未来支持Swift的开发。
@interface BBAXYZ : NSObject <BBAXYZEventProtocol>
- (id<BBAXYZEventProtocol>)page;
@end
然后,我们可以创建一个OC类来实现这个基础框架,并让所有的子类继承这个OC类并实现 page 方法,以返回适当类型的对象。这样,我们就可以在Swift中顺利地继承和使用这个基础框架。
例如:
@interface BBAABC : BBAXYZ
- (UIViewController<BBAXYZEventProtocol> *)page;
@end
需要注意的是,虽然这样的修改增加了轻量级的中间OC类,但它仍然实现了Swift与OC的混编,并允许我们在Swift中开发新的子类。这种方式既保证了代码的兼容性,又使得我们可以继续利用OC的优点。
class BBAEFG: BBAABC {
}
3.7 Swift调用OC接口,OC的nullability标注使用时的注意事项
问题场景:
1、OC 接口定义为 nonnull,swfit 调用时正常是当做不可选类型使用,这时如果 OC 接口不规范返回 nil,则出现运行时崩溃。
2、OC 接口未定义 nonnull 或 nullable,在这种情况下,编译器会将 OC 的指针类型当成是隐式解析可选类型(例如 String!)导入到 Swift 中。swift 调用时,OC接口如果返回 nil,将会因为隐式解析一个为 nil 的可选值导致运行时崩溃。
解决方式:
1、Swift 调用 OC 接口时,如果 OC 的接口声明为 nonnull 或未指定 nullability 时,只有明确 OC 接口不为空的情况下才可调用。
2、在 OC 环境下,将 nil 赋值给 nonnull 指针也没有关系,编译器只会产生警告。这就需要程序员按规范编写 OC 代码,正确使用 nullability 标注,并增加运行时判空的断言,以支持向后兼容。
GEEK TALK
04
其他常见问题
4.1 Xcode编译只提示编译错误,提示信息非常少
1、检查并保障所有依赖的组件都已经Module化了,如配置build settings;
2、在组件中新增Swift文件(空文件也行)。
4.2 由于组件开启了Library Evolution 导致的编译报错
错误显示:@objc' instance method in extension of subclass of 'xxxxx' requires iOS 13.0.0
这是由于组件开启了Library Evolution导致,开关BUILD_LIBRARY_FOR_DISTRIBUTION 控制的。
一个库开启了Library Evolution,在依赖链下游的库中将:
1、对它的类实现 @objc 子类。
2、对它的类使用 extension 实现 @objc 的方法(这在 UIKit 的 protocol 中经常会遇到)。
3、对它的类实现子类,并添加 @objc 方法,且方法中使用父类的类型作为参数。
这些功能是实现的局限。估计是需要在 Swift 运行时有一些对应的更改,所以只在 swift 5.1 (iOS 13)运行时里才可以运行。
除此之外,还会有一些编译时没有报错,但运行时 crash 或结果不正确的情况。
百度App中默认开启Library Evolution,一个组件关闭Library Evolution会导致二进制存在不兼容的情况,暂时无解决方案。
4.3 暴露的Private头文件如果使用双引号import,会报警告,需要修改为尖括号
如果使用import <xxxx.h>,Project下其他Target就引用不到了,如百度App中Debug模块引用此Private头文件时,会报错 not found with <angled> include, use "quotes" instead。
GEEK TALK
05
总结
以上是我们在Swift开发过程中所遇到的一些常见问题及其相应的解决方案。然而,随着我们不断深入Swift开发这片浩渺的海洋,更多独特的问题将会逐渐浮现。我们会持续将这些新问题以及其对应的解决方案整理并发布出来,为广大的开发者们提供有价值的参考。欢迎大家留言探讨。
作者:路涛、艳红
来源:微信公众号:百度Geek说
出处:https://mp.weixin.qq.com/s/hHH4a8tOukqhd9A5SxbzYw
家都说这世上最美的就是玫瑰和女人,当这两者“碰撞”在一起的时候,一切就变得美不可言了。想必大家也知道很多名人的美都与玫瑰结下了不解之缘,比如超模娜塔莉亚·沃迪亚诺娃。
可能是现实版的灰姑娘变成公主的故事,娜塔莉亚小时候靠摆水果摊维生,14岁那年在自家水果摊忙活时,被模特星探发掘,飞往法国巴黎,开启传奇超模生涯。
之后的她也经历过离婚,执意一个人带着三个孩子生活。但最后和贝尔纳·阿尔诺(顶级奢侈品公司LVMH )的儿子安托万.阿尔诺结婚,目前两人育有两个孩子,带着五个孩子一起生活。当然,于我们而言,她是幸运的。但如果没有足够的勤奋和自律,她可能还是一个水果摊的老板娘。
还记得当时刚生孩子的她,产后第三个星期她就回T台上了。反正从她出道到现在,肌肤状态一直都非常的好。看到她为某时尚杂志拍摄的getready with me 视频之后就发现原来皮肤好,很多原因是在于选对了产品呀。
可以说她的“本命“就是Fresh了吧,视频里所有的推荐几乎都是Fresh的单品呢~而今天小编就要来推荐一下Fresh家的东西了,感觉最近超多仙女急需一瓶好用的水哦,毕竟换季像清仓一样,是时候买瓶新的水给自己啦。
推荐产品:Fresh玫瑰花瓣水
今天小编就是要推荐这款超美的Fresh馥蕾诗玫瑰花瓣水,真真实实的玫瑰花哦,抢先用过的小编表示看到这么多玫瑰花心情都变好了!所以Fresh玫瑰花瓣水怎么样?
答案当然是肯定的,光不含酒精这一点就能让小仙女们爱上了好嘛!而且小编还想说连续使用Fresh馥蕾诗玫瑰花瓣水两周,肌肤的水润度真的得到了极大提升哦。连续早晚使用,涂抹瞬间,都能马上感受到水润修复的舒适感。
这瓶玫瑰水中含有超多真实的玫瑰花瓣,也正是因为如此,每天涂的时候都有一股天然的玫瑰花的香味,可以说是每天都在期待着这股玫瑰花的味道,恨不得一天涂N次,让肌肤和自己都一直在真正的花海中一样。
想必有盆友就要问了,Fresh玫瑰花瓣水功效是什么呢?别急,小编这就告诉你们哦。它不仅可以帮助肌肤补充水分,还能细致毛孔,对红肿的痘痘有消炎的效果哦。同时还能舒缓肌肤,改善肤质。让肌肤如花瓣一般,再现水润舒缓、细致柔嫩的肌肤,并为后续的护肤步骤做好准备。
使用方法也很简单哦!每日早晚洁净肌肤后,用化妆棉或指尖沾取适量玫瑰花瓣水,轻柔扫过面部与颈部就可以啦!所以经过小编的一番“讲解“,是不是觉得Fresh玫瑰花瓣水好用了呢?在这里小编还想说搭配同系列的Fresh产品效果会更好哦。
比如,玫瑰润泽密集保湿面霜可以为肌肤带来持续润养,深彻补水。当然再搭配玫瑰润泽保湿舒缓面膜,用完之后能让大家的肌肤看上去更加光彩柔嫩哦。大家可以根据自己的肌肤状况选择购买哦~这里小编就给你带来点福利啦,Fresh玫瑰花瓣水体验装免费试用申领 ~赶紧点击链接领取吧~
http://freshrose.comeyes.cn/index.html?utm_source=Onlylady&utm_medium=Onlyladyeditorial&utm_campaign=fresh2018
*请认真填写需求信息,我们会在24小时内与您取得联系。