标签归档:ios

resource fork, Finder information, or similar detritus not allowed

今天,我准备更新我的一个app,这个app很久以前上架的,当时还是iPhone 32位 CPU的时候,所以有必要更新一次。

然而,在编译过程中,我遇到了以下错误:

resource fork, Finder information, or similar detritus not allowed

经过一番搜索,找到了解决方案。

http://stackoverflow.com/questions/39652867/code-sign-error-in-macos-sierra-xcode-8-resource-fork-finder-information-or

https://developer.apple.com/library/content/qa/qa1940/_index.html

原来,MacOS的文件有三个fork,data fork, resource fork和Finder info。Data fork存储了文件的内容;resource fork保存了一些扩展信息,如什么应用创建了这个文件,又例如上次你打开这个txt文件的时候,正在显示的是第几行,等等; Finder Info则保存了文件所有者,创建者等信息。

从iOS 10和macOS Sierra开始,从安全考虑,app bundle中的文件将不能包含resource for和Finder info了。所以,我们必须去掉这两个信息,才能成功进行代码签名。具体如何去掉这两块信息,大家可以参考下面的说明。

40
Code signing fails with error 'resource fork, Finder information, or similar detritus not allowed'

Q:  When I build my app, code signing fails with the error "resource fork, Finder information, or similar detritus not allowed." What does this mean and what should I do about it?

A: This is a security hardening change that was introduced with iOS 10, macOS Sierra, watchOS 3, and tvOS 10.

Code signing no longer allows any file in an app bundle to have an extended attribute containing a resource fork or Finder info.

To see which files are causing this error, run this command in Terminal:

$ xattr -lr 

replacing  with the path to your actual app bundle.

Here's an example of this command in action:

$ xattr -lr Foo.app
/Applications/Foo.app: com.apple.FinderInfo:
00000000  00 00 00 00 00 00 00 00 00 10 00 00 00 00 00 00  |................|
You can also remove all extended attributes from your app bundle with the xattr command:

$ xattr -cr 

Note that browsing files within a bundle with Finder's Show Package Contents command can cause Finder info to be added to those files. Otherwise, audit your build process to see where the extended attributes are being added

iOS/Android的WebView中用file input支持拍照或选择相册的照片

如果我们的一个移动端的网页需要让用户上传一张照片,那么,通常而言,我们可以写了以下一段HTML代码

<input type='file' />

那么Mobile Web界面将会显示下面的一个控件,通过该控件,用户可以拍照或者选择手机中的文件而上传。

在iOS下,Mobile Safari会在你点击上面的控件之后弹出如下界面
Screen Shot 2017-03-12 at 下午6.44.31

在Android系统下,弹出的界面根据具体的Android版本(不同的厂商定制版本、不同的Android系统版本)不同而略有不同,不过,通常也会提供相机,相册等选项。下图为小米手机的例子。
IMG_20170312_184934

然而,如果你在原生的应用中内嵌了一个UIWebView/WKWebView(iOS)或者WebView(Android),你就会发现,原来在Mobile Safari和Chrome中可以正常运行的代码很有可能不能正常工作了。

在iOS系统下,你会发现,如果UIWebView所在的View Controller是通过presentViewController展示的,那么,你会发现,用户点击 后,你的iOS应用会出现一些奇怪的问题,如应用崩溃,如你在相册选择了照片之后,UIWebView所在的View Controller不见了,等等。

最后,我的解决方法是,不通过presentViewController展示UIWebView所在的View Controller,而是通过UINavigationController的pushViewController去展示View Controller。

而对于Android系统,应用内嵌的WebView本来就不支持使用文件上传功能,所以,<input type=’file’ />在Mobile Chrome上有效,到了应用内嵌的WebView中就无效了。

在Android上,我的解决方法是这样的,通过WebView的addJavascriptInterface注入一个camera对象,让WebView中的js代码通过camera对象从而唤起相机或者相册。

webView.addJavascriptInterface(new DemoJavaScriptInterface(), “demo”);

Javascript代码

window.demo.camera();

假设DemoJavaScriptInterface的是位于WebViewActivity里面的一个内部类,且WebViewActivity有一个私有变量String mCameraPhotoPath,那么,DemoJavaScriptInterface代码如下

 final class DemoJavaScriptInterface {
        DemoJavaScriptInterface() {
        }

        @JavascriptInterface
        public void camera() {

                Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
                if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
                    // Create the File where the photo should go
                    File photoFile = null;
                    try {
                        photoFile = createImageFile();
                        takePictureIntent.putExtra("PhotoPath", mCameraPhotoPath);
                    } catch (Exception ex) {
                        // Error occurred while creating the File
                        Log.e("WebViewSetting", "Unable to create Image File", ex);
                    }

                    // Continue only if the File was successfully created
                    if (photoFile != null) {
                        mCameraPhotoPath = photoFile.getAbsolutePath();
                        takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT,
                                Uri.fromFile(photoFile));
                    } else {
                        takePictureIntent = null;
                    }
                }

                Intent contentSelectionIntent = new Intent(Intent.ACTION_GET_CONTENT);
                contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE);
                contentSelectionIntent.setType("image/*");

                Intent[] intentArray;
                if (takePictureIntent != null) {
                    intentArray = new Intent[]{takePictureIntent};
                } else {
                    intentArray = new Intent[0];
                }

                chooserIntent = new Intent(Intent.ACTION_CHOOSER);
                chooserIntent.putExtra(Intent.EXTRA_INTENT, contentSelectionIntent);
                chooserIntent.putExtra(Intent.EXTRA_TITLE, "拍照或者选择图片");
                chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentArray);

                Activity mContext = WebViewActivity.this;
                if (Build.VERSION.SDK_INT >= 23) {
                    int checkCallPhonePermission = ContextCompat.checkSelfPermission(mContext, Manifest.permission.CAMERA);
                    if (checkCallPhonePermission != PackageManager.PERMISSION_GRANTED) {
                        ActivityCompat.requestPermissions(mContext, new String[]{Manifest.permission.CAMERA}, REQUEST_CODE_ASK_CAMERA);
                        return;
                    }
                }

                WebViewActivity.this.startActivityForResult(chooserIntent, 101);

            }

    }

WebViewActivity中的onActivityResult方法代码如下

@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (resultCode == Activity.RESULT_OK) {
            switch (requestCode) {
                case 101:
                    if(data == null){
                        String imageurl = mCameraPhotoPath;
                        //相机拍好的照片就保存在路径imageurl中
                    }
                    else{
                        Uri uri = data.getData();
                        //通过uri获取照片数据
                    }

                    break;

                default:
                    return;
            }
        }

    }

说说ITMS-90682: can’t contain 16-bit or P3 assets if the app supports iOS 8 or earlier

昨日,我在工作中听到我部门打包好的IPA,到了测试的时候,一旦测试的手机是iOS 8或者9.2的手机,应用总是会随机崩溃。最后,同事们找到了原因,具体原因见
http://stackoverflow.com/questions/39404285/xcode-8-build-crash-on-ios-9-2-and-below

When I build my app with Xcode 8 GM Seed and run it on an iOS 9.2 below device OR simulator, I get strange EXC_BAD_ACCESS crashes during app startup or a few seconds after the app launched. The crash always happens in a different spot (adding a subview, [UIImage imageNamed:], app delegate’s main method etc). I don’t get those crashes when I run it on iOS 9.3+ or 10 and I don’t get them when I build with Xcode 7 and run on iOS 9.2 and below. Has anyone else experiences something similar? Is this a known issue with Xcode 8?

当我用Xcode 8 GM Seed打包应用并在iOS 9.2或者更低版本的设备或模拟器运行的时候,我总是会在应用启动时或启动后的数秒之内遇到EXC_BAD_ACCESS,而崩溃的地方每次都不一样,如addSubView,[UIImage imageNamed:]甚至main方法。如果我在9.3或者10上运行,或者我用Xcode 7执行编译工作,然后运行在9.2以及以下的设备上,这些错误都不会发生。有没有人遇到类似的事情呢,这是不是Xcode 8的一个问题?

而解决的方法在这里讲了https://forums.developer.apple.com/thread/60919?start=0&tstart=0。简单说,Xcode认为你的图片资源中包含了16位或者P3的资源,所以打包的时候会进行相应的处理,但是,只有9.3或者更高版本才支持这样的资源,所以到了9.2就会崩溃。

此时,我想说说什么是P3资源。P3指的是DCI-P3或者说DCI/P3,一种数字电影投影所用的色域标准,它能比sRGB显示更多的颜色。苹果在推出5k显示屏的new iMac的时候(2015年10月),引入了DCI-P3,随后9.7寸的iPad Pro(2016年3月推出,推出时iOS 9.3系统)、iPhone 7(2016年9月推出,iOS 10系统)和2016的MacBook Pro都使用了DCI-P3色域。即苹果公司的iOS 9.3系统支持DCI-P3色域,而iPad Pro作为第一个支持DCI-P3的iPad,使用的是iOS 9.3。这也就解释了为什么我们之前打包的IPA到了9.2的设备上就会不正常。

那么,如何判断一个图片是不是P3的图片呢,其实很简单,就是要看这个图片的Color Profile是什么,通常来说,一个图片应该没有内嵌Color Profile或者内嵌sRGB的Color Profile。下面一个例子,我进行了两张图片的对照,左边为P3的png,右边为普通的png。P3和非P3图片对照

不难发现,左侧的图片有内嵌的Color Profile:Display,而右侧的常规的图片没有内嵌的Profile。那问题来了,为什么Color Profile: Display是P3资源呢?Display难道等于DCI-P3吗?

这里需要说明的是,Display指的是制作图片的人所使用的电脑的显示器所使用的Color Profile,具体这个Profile是什么,那要看制作者的电脑上的Display对应的Color Profile是什么,如果制作者使用的是5k的iMac,那么Display对应的Profile就是P3了。
下图为笔者的MacBook Pro的Photoshop新建一个图像的对话框。
display_and_display_p3
我们看到,默认的是Display,即用户当前显示器所使用的Color Profile,此外,还有许多Color Profile可以选择,如Display P3,这就是DCI-P3色域的Color Profile。如果你用的是5k的new iMac,那么Display就等于Display P3,如果你用的是别的Mac,比如笔者的2013的MacBook Pro,那么Display就等于sRGB。

下图为2016的MacBook Pro的技术规格,请大家注意“广色域(P3)”这几个字。这就是前面提到的P3 assets中P3的含义。
2016 MacBook Pro的显示屏采用了广色域P3

解决方法如下,摘录自https://forums.developer.apple.com/thread/60919?start=0&tstart=0,我就不翻译了。当然,最好是设计师在设计资源的时候,如果使用的是5k的iMac或者2016年新推出的MacBook Pro,那么,不要选择默认的Display选项,要选择sRGB,这样图片资源就可以用于支持iOS 8和iOS 9.2版本的工程了。

You can find 16-bit or P3 assets by running “assetutil” on the asset catalog named in the error message from iTunes Connect. The following steps outline the process:
1. Create an Inspectable .ipa file. In the Xcode Organizer (Xcode->Window->Organizer), select an archive to inspect, click “Export…”, and choose “Export for Enterprise or Ad-Hoc Deployment”. This will create a local copy of the .ipa file for your app.
2. Locate that .ipa file and change its the extension to .zip.
3. Expand the .zip file. This will produce a Payload folder containing your .app bundle.
4. Open a terminal and change the working directory to the top level of your .app bundle
cd path/to/Payload/your.app

5. Use the find tool to locate Assets.car files in your .app bundle as shown below:
find . -name ‘Assets.car’

6. Use the assetutil tool to find any 16-bit or P3 assets, in each Assets.car your application has as shown below. :
sudo xcrun –sdk iphoneos assetutil –info /path/to/a/Assets.car > /tmp/Assets.json

7. Examine the resulting /tmp/Assets.json and look for any contents containing “DisplayGamut”: “P3” and its associated “Name”. This will be the name of your imageset containing one or more 16-bit or P3 assets.

8. Replace those assets with 8-bit / sRGB assets, then rebuild your app.

Update: If your Deployment Target is set to either 8.3 or 8.4 and you have an asset catalog then you will receive this same error message, even if you do not actually have 16-bit or P3 assets. In this case you will either need to lower your Deployment Target to 8.2, or move it up to 9.x.

2016年12月31日后,NSAllowsArbitraryLoads将被禁用,建议所有iOS App的Web API都升级为HTTPS

苹果的iOS 10正式版本马上就要发布了,而自iOS 10开始,苹果对于网络安全将更加重视,2016年12月31日以后,你将不能用NSAllowsArbitraryLoads来禁止App Transport Security了。

简单说,苹果坚持,你的所有的Web API请求都必须使用https,而如果你需要使用WebView加载外部的网页,考虑到你无法控制外部的网站,苹果引入了新的NSAllowsArbitraryLoadsInWebContent,允许WebView可以加载任意的网页。

The new NSAllowsArbitraryLoadsInWebContent key for your Info.plist file gives you a convenient way to allow arbitrary web page loads to work while retaining ATS protections for the rest of your app.

如果你需要在App中加载任意的网页,那么,你可以在Info.plist中添加NSAllowsArbitraryLoadsInWebContent,这样,你可以加载非安全的http页面,同时保证你的app的其他方面仍然受到App Transport Security的保护。

对于许多公司来说,API团队近期有一项非常重要的工作,也就是,如果你们的Web API仍然是通过明文的http进行调用的,那么,你们需要尽快将API的endpoint升级为https。

如果大家真的来不及升级到https,那怎么办呢。我突然想到,苹果只是禁止了NSAllowsArbitraryLoads,但是我们还是可以NSExceptionDomains,将API对应的域名添加到例外列表中的。

The following listing represents the overall structure of the NSAppTransportSecurity dictionary, showing all possible keys, all of which are optional. Keep this structure in mind as you configure each element of the dictionary, as needed, for your app:
以下代码是NSAppTransportSecurity字典的结构模型。该模型列举了所有可能的键值对,这些键值对都是非必须的。您可以根据这个结构中所示的键值对的信息来编辑Info.plist中的NSAppTransportSecurity字典。

NSAppTransportSecurity : Dictionary {
    NSAllowsArbitraryLoads : Boolean
    NSAllowsArbitraryLoadsInMedia : Boolean
    NSAllowsArbitraryLoadsInWebContent : Boolean
    NSAllowsLocalNetworking : Boolean
    NSExceptionDomains : Dictionary {
         : Dictionary {
            NSIncludesSubdomains : Boolean
            NSExceptionAllowsInsecureHTTPLoads : Boolean
            NSExceptionMinimumTLSVersion : String
            NSExceptionRequiresForwardSecrecy : Boolean   // Default value is YES
            NSRequiresCertificateTransparency : Boolean
        }
    }
}

解决一个WKWebView在iOS 10上的Crash: Invalid parameter not satisfying: targetNode

近日,我遇到了一个让我感到匪夷所思的问题。App使用了WKWebView展示内容,然而,一旦iOS 10的用户手指触摸了WKWebView,app就会立即崩溃,而iOS 9的用户就不会遇到这个问题。

经过调试,我发现Stack Trace和Exception如下


Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid parameter not satisfying: targetNode'
*** First throw call stack:
(0x1864041c0 0x184e3c55c 0x186404094 0x186e8d82c 0x18cc98e4c 0x18c6f551c 0x18c78f364 0x18c20ec70 0x18c791b1c 0x18c791a78 0x18c790d34 0x18c20d34c 0x18c1ddf84 0x18c9b3008 0x18c9aca70 0x1863b2278 0x1863b1bc0 0x1863af7c0 0x1862de048 0x187d5f198 0x18c248bd0 0x18c243908 0x1000e58a0 0x1852c05b8)
libc++abi.dylib: terminating with uncaught exception of type NSException

而调试信息显示问题出现在UIGestureGraphEdge.m的第25行。

Assertion failure in -[UIGestureGraphEdge initWithLabel:sourceNode:targetNode:directed:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit/UIKit-3599.6/Source/GestureGraph/UIGestureGraphEdge.m:25

这个文件是Apple的UIKit的一部分啊,难道这是Apple iOS 10的bug?

不管是不是iOS的系统的bug,我想还是找到一个解决方案或者说workaround的好。

最后发现,我原来在app启动的时候,在+(void)load方法中先创建好WKWebView,等到viewDidLoad的时候直接将WKWebView通过addSubView添加到View Controller的view上,这样就可以在viewDidLoad方法中节约了创建WKWebView的时间,可是这样子创建好的WKWebView,在iOS 10上,用户一旦进行了触摸操作,就会出现前面提到的崩溃。

如果我将创建WKWebView的时机稍微延后一些时间,如在load方法中,通过dispatch_after延迟1到2秒再创建WKWebView,那么,WKWebView就可以正常处理用户的触摸操作,不会崩溃。

static NSMutableArray *webViews;
#define WKWebView_Cache_Create_Delay 2
+ (void)load{
    if(SYS_VERSION_LESS(9))return;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSUInteger createDelay = 0;
        if(SYS_VERSION_GREATER_OR_EQUAL(10)){
            createDelay = WKWebView_Cache_Create_Delay;
        }
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(createDelay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            webViews = [[NSMutableArray alloc] init];
            [webViews addObject:[self createWKWebView]];
            [webViews addObject:[self createWKWebView]];
        });
        
    });

此外,在https://forums.developer.apple.com/thread/61432上,也有人遇到了只在iOS 10上才出现的Crash,而Crash的代码的位置也是UIGestureGraphEdge.m的第25行。


flykk
Aug 28, 2016 11:54 PM
When I clicked the NavigationBar in my App,it crashed.It only crashes in iOS 10 .The console log is:
Assertion failure in -[UIGestureGraphEdge initWithLabel:sourceNode:targetNode:directed:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit/UIKit-3599.6/Source/GestureGraph/UIGestureGraphEdge.m:25

翻译如下
当我点击我的app的NavigationBar的时候,它就崩溃了。这仅仅发生在iOS 10上。控制台的日志是
Assertion failure in -[UIGestureGraphEdge initWithLabel:sourceNode:targetNode:directed:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit/UIKit-3599.6/Source/GestureGraph/UIGestureGraphEdge.m:25

说实话,这个现象我没到过,但是,崩溃相关的文件和行数确是一样的。期待这位朋友找到解决方法。

iOS UILabel显示了重叠文字(gets combined, superimposed, overlapped)

Screen Shot 2016-06-30 at 12.07.11 AM

今天调试遇到了一个问题,上图是一个UILabel的截图,该UILabel的text属性实际上是“21张”,然而,如果你仔细看这张截图的话,似乎可以看出,大约是一个“21张”文字的渲染结果叠加了一个“10张”文字的渲染效果。通常,出现这种情况,我的第一反应是我是不是重复的addSubView了,导致多个UILabel相重叠了。

然而,仔细检查了代码,并且用Reveal查看了View层次结构后,我确认了一点,没有多个UILabel重叠,确实只有那一个UILabel并且就是该UILabel把“21张”渲染成了上图的模样。

最后发现,问题在于UILabel的backgroundColor不能设置为nil,也不能设置为透明色。只要给UILabel设置了背景色,这个问题就解决了。

而且,我发现这不是个例,早在2012年,2013年就已经有人遇到这个问题,而且,根据Stackoverflow.com提供的线索,这似乎是iOS 5开始才有的问题(看后面的If I try it on an iPhone 4, the previous text of the label doesn’t disappear)。如今,iOS 10都快要正式版了,没想到这个问题还在。

最后,给大家忠告,一定要仔细检查UILabel的背景,千万不要把backgroundColor设置成透明色或者nil。

参考资料如下

http://www.cocoachina.com/bbs/read.php?tid=257298
主题 : UILabel背景为透明时,刷新文字会和旧文字重叠
楼主 :
发表于: 2013-03-06 17:04

把label的背景色从默认(也就是clear color)设成其他颜色,问题就解决了。

如果有时候需要label背景透明,那又该怎么办呢?

http://stackoverflow.com/questions/2271144/setting-transparent-background-for-uilabel-for-iphone-application

Nullifying the backgroundColor works fine at first, but if you subsequently change the label’s text, the new text will be superimposed on the old. Very weird. – Wienke Sep 27 ’12 at 19:50 (编者注:这是2012年9月27日的评论,原问题发表于2010年4月29日。将backgroundColor属性设置为nil,最开始你会发现能如你所愿,然后,随后你就会发现,如果你更改了label的text属性,新的文本会重叠在旧的文本之上。非常奇怪。)

http://stackoverflow.com/questions/10373119/uilabels-new-text-gets-combined-with-previous-text-when-testing-on-real-iphone#

I have some UILabel objects in my app, and I change their value when a button is pressed. It works fine in the simulator and on old iPhones, but If I try it on an iPhone 4, the previous text of the label doesn’t disappear, it shows behind the the new text (well, sometimes it disappears and only the correct text appears, but most of the times it doesn’t work right). (编者注:提问者说道,在模拟器和比iPhone 4老的iPhone上,一切正常,可是,如果是iPhone 4,UILabel设置了新的文本的话,旧的文本仍然显示在新的文本的后面,这正如本文最开始的截图所显示的那样。)
asked Apr 29 ’12 at 14:53

I can tell you it’s not a bug, and not common. I’m guessing that you have a transparent background behind UILabel? And redraw might not be correctly handled in the background view. You might want to post more about your view hierarchy. – He Shiming Apr 29 ’12 at 17:08 (编者注:后面有人评论,我可以告诉你,这不是bug,也不常见。 我猜测你的UILabel一定有一个透明的背景,而redraw操作或许不能正确处理透明背景的情况。你或许应该告诉大家你的视图层次结构的详细信息。)

如何用grep命令筛选,查找iOS的日志console log?

在iOS开发中,除了断点之外,一个最常见的调试方法就是查看iOS的log,我们在代码中经常会通过NSLog输出大量日志,然后查看日志,看看是否出现了什么异常情形。

然而,随着项目不断进展,iOS的log也越来越多,这导致我们常常需要在冗长的log中寻找需要的某几行log。令人感到遗憾的是,Xcode的Console只有一个find功能,其他功能都没有。

如果我们可以像在命令行一样用grep等工具对iOS的console进行筛选,那该多好啊。

我经过搜索,终于发现了这个工具https://github.com/rpetrich/deviceconsole

这是一个可以在Mac上显示iOS的console的工具,github上保存的是工具的源代码,编译后,你就得到一个名为deviceconsole的可执行文件,运行deviceconsole,它就将usb连接到Mac上的iphone的console输出到了当前的terminal中,我只要执行deviceconsole | grep “Hello World”,就可以只查看带Hello World文字的日志了。

此外,还可以deviceconsole > /tmp/console.log,这样子iOS的console就会重定向到/tmp/console.log了。

如果你希望只显示带“Hello World”的日志,你可以执行命令
tail -f /tmp/console.log | grep “Hello World”

如果你想要看之前输出的带有“Hello World”的日志,那么你可以执行命令
grep “Hello World” /tmp/console.log