2014年11月08号,心城v1.1.0通过了苹果的审核。如果阅读过前面心城1.1.0今天提交审核了的朋友们会发现,时间俨然过了2周多。期间发生了什么事情?正常的Waiting For Review进入InReview大概1周差不多,这次怎么两周多了?

被拒了一次。

来看下被拒的原因吧,希望后面的朋友别和我一样。

1.问题描述

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
Reasons
2.23: Apps must follow the iOS Data Storage Guidelines or they will be rejected
3.3: Apps with names, descriptions, screenshots, or previews not relevant to the content and functionality of the App will be rejected
----- 2.23 -----

We found that your app does not follow the iOS Data Storage Guidelines, which is required per the App Store Review Guidelines.

In particular, we found that on launch and/or content download, your app stores 2.04MB. To check how much data your app is storing:

- Install and launch your app
- Go to Settings > iCloud > Storage & Backup > Manage Storage
- If necessary, tap "Show all apps"
- Check your app's storage

The iOS Data Storage Guidelines indicate that only content that the user creates using your app, e.g., documents, new files, edits, etc., should be backed up by iCloud.

Temporary files used by your app should only be stored in the /tmp directory; please remember to delete the files stored in this location when the user exits the app.

Data that can be recreated but must persist for proper functioning of your app - or because customers expect it to be available for offline use - should be marked with the "do not back up" attribute. For NSURL objects, add the NSURLIsExcludedFromBackupKey attribute to prevent the corresponding file from being backed up. For CFURLRef objects, use the corresponding kCFURLIsExcludedFromBackupKey attribute.

For more information, please see Technical Q&A 1719: How do I prevent files from being backed up to iCloud and iTunes?.

It is necessary to revise your app to meet the requirements of the iOS Data Storage Guidelines.

----- 3.3 -----

In addition, your marketing screenshots do not sufficiently reflect the app in use, which does not give the user an accurate understanding of what the app does, as required by the App Store Review Guidelines.

We’ve attached the relevant screenshot(s) for your reference.

It would be appropriate to revise your screenshots to demonstrate the app functionality in use.

If your iTunes Connect Application State is Rejected, a new binary will be required. Make the desired metadata changes when you upload the new binary.

For discrete code-level questions, you may wish to consult with Apple Developer Technical Support. When the DTS engineer follows up with you, please be ready to provide:

- complete details of your rejection issue(s)
- screenshots
- steps to reproduce the issue(s)
- symbolicated crash logs - if your issue results in a crash log

一共两个原因。
分别对应苹果审核规则的2.23和3.3规则。

2.先说3.3 元数据问题

截图不够丰富,表达不了应用的功能,增加了3张图片解决问题。

3.再谈2.23 数据存储问题

心城将用户拍照以及头像设置的图片以及用户的录音文件都保存在了沙箱Documents文件夹,MagicalRecord用的数据库文件默认存放在了Library/Application Support目录下。

我举个例子来说明:

Documents下的文件是会被 iCloud 自动备份的,苹果的测试人员发现,他们拍了一些照片,录制一些音频,然后退出应用,同步iCloud发现数据有2.04M,很明显,照片和音频数据以及数据库文件(存放在Library下的ApplicationSupport下,同样会被iCloud自动同步)是造成这2.04M的罪魁祸首,所以被拒。

究其原因是我存储文件的位置不对或者说是规则不对造成的。为了避免被 iCloud 同步,需要对这些文件进行“不备份标记”。将文件夹标记为不备份,那么其子文件和子文件夹都不会被备份。这里还不敢对 Documents 标记不备份,那样“太过分了”,毕竟还有其他数据和文稿需要备份。对Documents下的不需要的文件夹进行标记即可。

和一个朋友交流中,他提到了他以前犯过的一个错误,他的应用在访问网络时,把一些图片存放在了 Documents 文件夹里面了,也是导致被拒,这些图片应该视情况放在tmp或者cache里面。

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
//沙盒Documents目录路径
#define DOC_PATH ([NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0])
//沙盒Cache目录路径
#define CACHE_PATH ([NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0])
//沙盒Library目录路径
#define LIB_PATH ([NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0])
//沙盒Temp目录路径
#define TEMP_PATH (NSTemporaryDirectory())
// app data directory name
#define APP_DATA_PHOTOS_DIR_NAME @"photos"
#define APP_DATA_AUDIOS_DIR_NAME @"audios"
#define APP_DATA_AVATARS_DIR_NAME @"avatars"

-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
//.....
[self addSkipBackup];
//.....
}

-(void)addSkipBackup
{
NSString* photoDirPath = [DOC_PATH stringByAppendingPathComponent:APP_DATA_PHOTOS_DIR_NAME];
NSString* audioDirPath = [DOC_PATH stringByAppendingPathComponent:APP_DATA_AUDIOS_DIR_NAME];
NSString* avatarDirPath = [DOC_PATH stringByAppendingPathComponent:APP_DATA_AVATARS_DIR_NAME];

[self addSkipBackupAttributeToItemAtURL:[NSURL fileURLWithPath:photoDirPath]];
[self addSkipBackupAttributeToItemAtURL:[NSURL fileURLWithPath:audioDirPath]];
[self addSkipBackupAttributeToItemAtURL:[NSURL fileURLWithPath:avatarDirPath]];
[self addSkipBackupAttributeToItemAtURL:[NSURL fileURLWithPath:[NSPersistentStore MR_applicationStorageDirectory]]];
}

-(BOOL)addSkipBackupAttributeToItemAtURL:(NSURL *)URL
{
assert([[NSFileManager defaultManager] fileExistsAtPath: [URL path]]);

NSError *error = nil;
BOOL success = [URL setResourceValue: [NSNumber numberWithBool: YES]
forKey: NSURLIsExcludedFromBackupKey error: &error];
if(!success){
NSLog(@"Error excluding %@ from backup %@", [URL lastPathComponent], error);
}
return success;
}

修改之后,重新提交,漫长的WaitingForReview又重新开始。今天进入了InReview后顺利通过上架。

4.杂说

在等待的期间,v1.2.0也在另外的分支开发着。今天1.1.0上架后,v1.2.0 也做了提交。

v1.2.0的改动
1.崭新的UI界面;
2.增加密码锁保护隐私;
3.消除了5连拍时存在的闪退隐患。

襁褓中的心城,并不完美也并不强大,虽然收到了负面的反馈,但不妨碍我把TA坚持做下去做好的决心。

行走在黑暗中,也许某天,会看到光明。




心城

在心城前期的开发过程中,因为暂时还没有美术人员,所以UI上我们会选择一些”开源“的一些好的设计。这里不得不提 mariodelvalle 的 http://mariodelvalle.github.io/CaptainIconWeb/ 这些优秀的设计。网站中提供有所有ICON的PSD源文件,甚至也包括了EPS、SVG矢量源文件,方便了在Illustrator下无限放大不失精度的要求。

感谢开源!

设计方面,我们会保持一个扁平的风格,一个是现在流行,另外扁平的风格的确看着特别舒服。在接受程度方面,IOS设备的使用者随着IOS7、IOS8的升级使用,对于扁平化style基本上都处于一个认可并喜欢的态度。

那下面 v1.2.0 这个的版本,我们会在细节和UI上处理的更好,敬请期待。也非常期待身边朋友和设计人员能给提出宝贵的意见!


海纳百川,有容乃大。心城会在一路的荆棘和鼓励下逐渐成长。

今天是2014年10月22号,距离上次心城1.0.0版本的提交快到了三周。在这三周里,我们围绕着身边朋友的反馈以及我们预计要做的功能,完成了1.1.0版本。






看看我们的成果!新加功能,以后会有更多!

1、增加了自定义背景色,颜色随心随手随时换;
2、增加了文本、照片两种表达方式,爱写字爱拍照的可以大展拳脚了;
3、增加了删除人物功能,请慎用;
4、调整了UI,常用功能轻点即立刻弹出,长按头像可展开更多功能。


小插曲

本来是昨天下午提交的,今天早上起来,看到一哥给我发的qq消息,我一看时间是凌晨1点多发的。他指出了新提交版本的一个问题,我核查了下代码,这个问题确实存在。

那么问题来了。

遇到这种情况,我们开发人员需要做什么工作?

1.关于代码方面

我一直用的git来进行代码版本控制,新建一个“1.1.0bugFix”分支后面进行合并或者在原1.1.0分支上进行修改都可以。关键是重现 BUG,找到代码问题所在,修复后,再次测试。没问题后,重新在Xcode里面Archive,上传到ITC后台。

2.ITC(ItunesConnect)方面的工作

一般这种情况,我们需要将提交的 APP,自己手动 reject(拒绝)掉。因为ITC网站苹果改版了,最新的网站界面和以前的有很大不同,可能有时候遇到问题,google出来的解决办法或许并不使用。

进入 ITC 后台,在 MyApps 里面,

将刚提交的版本,点击remove this version from review,将刚提交的版本从审核队列中移除掉。我们需要选择我们刚上传的app就可以了,而其他的元数据资料不用改动。所以在下方的Build一栏中,将之前的 build 删除,后面要做的就是重新 add 我们新上传的 build。

到这里有个问题,由于我们新上传 app,版本号仍然要保持 1.1.0,如果只修改了代码,就直接上传到ITC,在上传检查中就会报错,说你的这个 1.1.0 的 build 已经存在。问题出在了 build 的数字上面。第一次的1.1.0,在xcode项目中,Version为1.1.0,Build我也填成了1.1.0,而修改代码后,我忘记修改了Build的数字,所以会出现上面的报错信息。

我脑海中第一个反应,能不能在ITC里面把我上传的Version 1.1.0,Build 1.1.0这个提交的app prelease 删除掉。在Stackoverflow上我找到了答案,不能删除。解决方法很简单,我们重新Archive的时候,修改下Build的数值就可以了。ITC里面会记录上你同一个版本的所有Build,以Build数值来区分。

Build的填写没有很大要求,我这里改成了1.1.0.1。重新提交,这次顺利上传。

在Xcode中的设置

在 MyApps 的 Prelease 一栏下,我们会看到同个版本的两次Build


作为一个ios开发新入门的选手,在开发的过程中遇到了许多问题,有些问题我记录在了印象笔记里,但我觉得仍然有必要把我一路过来遇到的杂七杂八的问题与大家一起分享下,在后面的文章中会陆续记录些我在心城的开发中,遇到的技术问题以及一些想法。

也许你曾经遇到过,也许你没有,能一起有所收获,这么做便值了。

今天,《心城》的1.0.0版本总算正式在App Store上发布了,这里是地址。因为我前期对应用开发的不熟悉,所以花了不少时间才敢推出最基本的功能。暂时没有美术,设计上也欠缺一些。

它既迟,也令我们感到羞愧。

这么说,是因为有个老外那么说:如果你发布第一个版本时没有感到羞愧,那么它发布的一定是太晚了。

现有的功能,已经有用户反馈说感觉无事可做。我们只能这么解释:它不是游戏,是不会粘用户的。当用户想起某人,感觉这人是重要的,就可以在心城里添加头像,录上一段话。它不是必需的,取决于用户表达情感的意愿。决不至于让用户自作多情。

所以作为一个目前只有不到10M的应用,装一个,它会静静在那里等着你某个情感漫溢的瞬间。

实用情境

1、总有那么一两个、三四个至亲至近的人;

2、忽然念想起某人,也不知道现在怎样了;

3、好喜欢好声音里的**,就粉TA了;

4、梦到过世的姥姥;

5、喜欢对面的新同事

……

按住头像,可以录音,可是说点什么呢?

1、什么都不说就确定、退出录音界面,欲言又止也是一种选择;

2、说出你的思念;

3、答应为TA做一件事却不必直接告诉TA;

4、说“对不起”;

5、祝福,祈祷,愿TA一切都好;

……

人们都太忙了。

1、打开心城啥都不干,看着一个个头像发发呆;

2、翻看下过往的记录,慢慢回忆;

3、想想是不是该联络联络那谁谁;

4、那答应谁的事情差点忘了;

5、深情抚摸那谁的头像;

……

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
我不怕板砖,我只是认真。相信后面的版本迭代,会让心城成熟起来。

补充说明

【心城】 这个关键字不是特别好,在搜索方面有些吃亏。不少应用的关键字里面,带有心字,如果在 App Store 里面搜索,输入“心城”后,类别选择【社交】,可以快速的找到。

在app开发的时候,有时候需要用到ios模拟器中的相册图片。添加的方法非常简单,做个备注。

让模拟器显示Home页,拖放电脑中的图片到模拟器中,这时候模拟器的Safari浏览器会打开该图片,轻按图片数秒,会有保存菜单,保存即可。

我们接着上一篇 深入浅出MagicalRecord-03

这节我们来一起学习下 MagicalRecord 对数据的存储,内容主要来自于 MagicalRecord的github资料

存储的时机

一般情况下,我们应该在数据发生变化时就进行存储操作。有些应用选择在退出的时候存储,然而在大多数情况下这是不必要的。事实上,如果你只是当应用退出的时候进行存储,你有可能会丢失数据!如果你的应用崩溃了呢?用户会丢失他们改变的数据,这是很糟糕的体验,应该极力去避免出现这种情况。

如果你发现存储比较耗时,有下面几点你可以考虑下:

1.利用后台线程存储

MagicalRecord 提供了一个简洁的API来操作后台线程对实体改变的存储。例如:

1
2
3
4
5
6
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
//在这里做存储工作,该闭包代码块的工作将会在后台线程运行
} completion:^(BOOL success, NSError *error) {
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];

2.将存储任务拆分成细小的存储

类似导入大量数据这样的任务应该被拆分成多个小模块。一次性存储多少量的数据并没有统一的标准,所以你需要使用 Apple’s Instruments 的来测试下你的应用的性能。

处理长时存储

ios平台

当退出 ios 应用时,有机会来整理和存储数据到磁盘上。如果你知道存储操作要持续一会,那么最好的方法就是请求应用延期退出。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
UIApplication *application = [UIApplication sharedApplication];

__block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];

[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
//这里做存储操作
} completion:^(BOOL success, NSError *error) {
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];

请确保认真阅读了 read the documentation for beginBackgroundTaskWithExpirationHandler,因为不适当或者不必要的延长应用程序的生命周期可能会在审核的时候遭到拒绝。

OSX平台

在 OS X Mavericks (10.9) 以及后面的版本中,App Nap 可以使得应用在后台的时候可以有效的被终止退出。如果你知道存储操作要持续一会,那么最好的方法就是暂时禁用自动终止和突然终止功能(前提是你的应用支持这些功能):

1
2
3
4
5
6
7
8
9
10
11
NSProcessInfo *processInfo = [NSProcessInfo processInfo];

[processInfo disableSuddenTermination];
[processInfo disableAutomaticTermination:@"Application is currently saving to persistent store"];

[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
//这里做存储操作
} completion:^(BOOL success, NSError *error) {
[processInfo enableSuddenTermination];
[processInfo enableAutomaticTermination:@"Application has finished saving to the persistent store"];
}];

和 ios 实现一样,在实现之前确保阅读 read the documentation on NSProcessInfo来避免被拒绝。

变化

在 MagicalRecord 2.2 中,存储API更加一致并遵循CoreData的命名模式。在这个版本中,已经加入了自动化测试来确保将来更新时,存储工作(新的API和废弃的API)能够继续工作。

MR_save被暂时恢复到当前线程的同步运行和存储到持久存储(persistent store)的原始状态。然而,MR_save方法被标记为“将被废弃”(deprecated),将会在下个大版本 MagicalRecord 3.0 中移除掉。你应该使用MR_saveToPersistentStoreAndWait同功能函数来替代它。

a)新的方法

新增加了下面几个方法:

NSManagedObjectContext+MagicalSaves

1
2
3
4
5
- (void) MR_saveOnlySelfWithCompletion:(MRSaveCompletionHandler)completion;
- (void) MR_saveToPersistentStoreWithCompletion:(MRSaveCompletionHandler)completion;
- (void) MR_saveOnlySelfAndWait;
- (void) MR_saveToPersistentStoreAndWait;
- (void) MR_saveWithOptions:(MRSaveContextOptions)mask completion:(MRSaveCompletionHandler)completion;

MagicalRecord+Actions

1
2
3
4
5
+ (void) saveWithBlock:(void(^)(NSManagedObjectContext *localContext))block;
+ (void) saveWithBlock:(void(^)(NSManagedObjectContext *localContext))block completion:(MRSaveCompletionHandler)completion;
+ (void) saveWithBlockAndWait:(void(^)(NSManagedObjectContext *localContext))block;
+ (void) saveUsingCurrentThreadContextWithBlock:(void (^)(NSManagedObjectContext *localContext))block completion:(MRSaveCompletionHandler)completion;
+ (void) saveUsingCurrentThreadContextWithBlockAndWait:(void (^)(NSManagedObjectContext *localContext))block;

b)标记为废弃的函数

下面这些函数被标记为“废弃的”,将会在 MagicalRecord 3.0 版本移除掉,推荐使用替代的函数。

NSManagedObjectContext+MagicalSaves

1
2
3
4
5
6
7
8
- (void) MR_save;
- (void) MR_saveWithErrorCallback:(void(^)(NSError *error))errorCallback;
- (void) MR_saveInBackgroundCompletion:(void (^)(void))completion;
- (void) MR_saveInBackgroundErrorHandler:(void (^)(NSError *error))errorCallback;
- (void) MR_saveInBackgroundErrorHandler:(void (^)(NSError *error))errorCallback completion:(void (^)(void))completion;
- (void) MR_saveNestedContexts;
- (void) MR_saveNestedContextsErrorHandler:(void (^)(NSError *error))errorCallback;
- (void) MR_saveNestedContextsErrorHandler:(void (^)(NSError *error))errorCallback completion:(void (^)(void))completion;

MagicalRecord+Actions

1
2
3
4
+ (void) saveWithBlock:(void(^)(NSManagedObjectContext *localContext))block;
+ (void) saveInBackgroundWithBlock:(void(^)(NSManagedObjectContext *localContext))block;
+ (void) saveInBackgroundWithBlock:(void(^)(NSManagedObjectContext *localContext))block completion:(void(^)(void))completion;
+ (void) saveInBackgroundUsingCurrentContextWithBlock:(void (^)(NSManagedObjectContext *localContext))block completion:(void (^)(void))completion errorHandler:(void (^)(NSError *error))errorHandler;

我们接着上一篇 深入浅出MagicalRecord-02

这节我们来一起学习下MagicalRecord对数据的增删改查,内容主要来自于 MagicalRecord的github资料

1. 增-创建实体

创建实体
1
Person *myPerson = [Person MR_createEntity];
指定创建的上下文中创建实体
1
Person *myPerson = [Person MR_createInContext:otherContext];

2. 删-删除实体

删除一个实体
1
[myPerson MR_deleteEntity];
删除特定上下文中的实体
1
[myPerson MR_deleteInContext:otherContext];
删除所有实体
1
[Person MR_truncateAll];
删除特定上下文中的所有实体
1
[Person MR_truncateAllInContext:otherContext];

3. 改-修改实体

1
2
Person *person = ...;
person.lastname = "xxx";

4. 查-查询实体

查询的结果通常会返回一个NSArray结果。

a) 基本查询

从持久化存储(PersistantStore)中查询出所有的Person实体
1
NSArray *people = [Person MR_findAll];
查询出所有的Person实体并按照 lastName 升序(ascending)排列
1
NSArray *peopleSorted = [Person MR_findAllSortedBy:@"lastName" ascending:YES];
查询出所有的Person实体并按照 lastName 和 firstName 升序(ascending)排列
1
NSArray *peopleSorted = [Person MR_findAllSortedBy:@"lastName,firstName" ascending:YES];
查询出所有的Person实体并按照 lastName 降序,firstName 升序(ascending)排列
1
2
3
NSArray *peopleSorted = [Person MR_findAllSortedBy:@"lastName:NO,firstName" ascending:YES];
//或者
NSArray *peopleSorted = [Person MR_findAllSortedBy:@"lastName,firstName:YES" ascending:NO];
查询出所有的Person实体 firstName 为 Forrest 的实体
1
Person *person = [Person MR_findFirstByAttribute:@"firstName" withValue:@"Forrest"];

b) 高级查询

使用NSPredicate来实现高级查询。

1
2
NSPredicate *peopleFilter = [NSPredicate predicateWithFormat:@"department IN %@", @[dept1, dept2]];
NSArray *people = [Person MR_findAllWithPredicate:peopleFilter];

c)返回 NSFetchRequest

1
2
NSPredicate *peopleFilter = [NSPredicate predicateWithFormat:@"department IN %@", departments];
NSFetchRequest *people = [Person MR_requestAllWithPredicate:peopleFilter];

d)自定义 NSFetchRequest

1
2
3
4
5
6
7
NSPredicate *peopleFilter = [NSPredicate predicateWithFormat:@"department IN %@", departments];

NSFetchRequest *peopleRequest = [Person MR_requestAllWithPredicate:peopleFilter];
[peopleRequest setReturnsDistinctResults:NO];
[peopleRequest setReturnPropertiesNamed:@[@"firstName", @"lastName"]];

NSArray *people = [Person MR_executeFetchRequest:peopleRequest];

e)查询实体的个数

返回的是 NSNumber 类型
1
NSNumber *count = [Person MR_numberOfEntities];
基于NSPredicate查询条件过滤后的实体个数
1
NSNumber *count = [Person MR_numberOfEntitiesWithPredicate:...];
返回的是 NSUInteger 类型
1
2
3
4
+ (NSUInteger) MR_countOfEntities;
+ (NSUInteger) MR_countOfEntitiesWithContext:(NSManagedObjectContext *)context;
+ (NSUInteger) MR_countOfEntitiesWithPredicate:(NSPredicate *)searchFilter;
+ (NSUInteger) MR_countOfEntitiesWithPredicate:(NSPredicate *)searchFilter inContext:(NSManagedObjectContext *)

f)合计操作

1
2
3
4
5
NSInteger totalFat = [[CTFoodDiaryEntry MR_aggregateOperation:@"sum:" onAttribute:@"fatCalories" withPredicate:predicate] integerValue];

NSInteger fattest = [[CTFoodDiaryEntry MR_aggregateOperation:@"max:" onAttribute:@"fatCalories" withPredicate:predicate] integerValue];

NSArray *caloriesByMonth = [CTFoodDiaryEntry MR_aggregateOperation:@"sum:" onAttribute:@"fatCalories" withPredicate:predicate groupBy:@"month"];

g)从指定上下文中查询

1
2
3
4
5
NSArray *peopleFromAnotherContext = [Person MR_findAllInContext:someOtherContext];

Person *personFromContext = [Person MR_findFirstByAttribute:@"lastName" withValue:@"Gump" inContext:someOtherContext];

NSUInteger count = [Person MR_numberOfEntitiesWithContext:someOtherContext];
下一篇:深入浅出MagicalRecord-04

2012年3月份的时候,当我终于把欠大学3年学费还掉拿到毕业证后,我离开了北京来到了上海。离开了众享乐学教育,离开了可爱的同事们,离开了好哥们,还好上海还有个铁哥们在。

来到上海,第一件事就是换电话卡了。在我换号后,短信和qq通知了家人和朋友们,便开始了我的找工作之旅。大学的专业是计算机,那时候对计算机编程却一点也不感冒,偏要跷课去自学动画,毕业后3年都在从事动画相关的工作。来到上海后,我仔细思考了下我自身的状况,发现并不适合从事动画。

何去何从?我思考了很久。

我还是回到了大学的专业,起码除下动画,还有游戏开发是我一直想探寻的宝地。在从事动画的时候,大部分情况在flash下工作,所以也接触到了flash的当家语言actionscript。所以在网投简历的时候,我定了个方向就是actionscript的游戏开发。而2012年的时候,actionscript游戏开发,当属页游最火了。经过一些面试后,我最后被上海视速录用。

刚进公司,接手的是一个棋牌游戏。很快经过几个月的工作学习,我重新拾起了程序。2013年初的时候开始了一个手游项目,集棋牌游戏+卡牌游戏的玩法于一体的《妙手牛牛》。2013年中下旬的时候游戏基本开发完毕,当然中途我们遇到了很多问题。产品经理对产品质量包括UI、游戏玩法要求很严格,中途的美术曾经大换血一次,设计也推翻了很多。但最终还是完成了。

公司的主营业务是视频聊天,游戏是正在拓展的业务。但游戏开发时,公司的视频应用也在同步开发,包括了ios和android版本。游戏开发结束时,ios版本基本也快结束了。公司要大力推广视频应用,游戏方面推广投入有风险,所以就搁置了。从那时候情况看,在视速公司体制下,从事游戏开发到上线推广运营,实话说,并不是很顺利的事情。

于是,产品经理也就是现在我的合伙人(我叫他一哥,因为名字中带“意”),2013年11月份时带着我离开了公司,两个美术和1个策划朋友并没有加入到我们的创业队伍。

小时候玩红白机、街机的童年时光是让我坚持游戏开发的很大理由,也是我跟随一哥出来创业的一个很“幼稚的理由”。我完全可以选择不来创业,找公司继续打工着。但也许骨子里有一份不安分的心,是我这么做的另外一个原因。大四的时候就和几个朋友开动画工作室,算是很不成熟的创业,不过最后因为资金问题没能继续做下去。

一哥和我从公司离职后,便在朋友的公司找了两个工位,这样基本持续了4个月左右。在这4个月中,我们一共开发出了两款游戏,一个是垃圾回收方面的公益游戏,另外一个是《三国纵》,我们吃饭讨论,走路讨论,在家视频语音讨论游戏的创意和玩法,最终游戏的原型基本成熟完成。为什么说是原型,因为美术我们没有任何投入。

游戏原型片段

在游戏圈创业,才深知这个行业的浮躁,才深知这个行业的浮浮沉沉。新闻报道中充斥着某某团队某某作品大卖,团队被几百万收购的新闻…,这些总是或多或少会对自己形成心里冲击。有正面的,也有负面的。而我们两个人,我自己的积蓄也逐渐花光,一哥保证了我的基本生活。寻找投资、寻找美术合作是我们当时谈的较多的话题。但事情总是没有那么顺利,我们遇到了很多障碍,而资金就是最大的问题。我们是想做精品游戏,而对美术有着较高的要求,为了寻找合适的美术,花费了不少时间。而寻找投资,更是没那么容易。

最后我们选择了放弃。为何就这么轻易放弃?我很不甘心。也许是游戏的玩法并没有预期中的那么惊艳,也许是料想到游戏上线后也只是大海中的苍茫一粟,反响平平,也许寻找美术、投资时遇到了很多阻碍,总是有很多理由来说明这个问题。也许是我们真的不行吧。

Maybe,我淡然一笑。凌晨3点,我瞪着屏幕,点燃了从哥们那拿的一根烟。我做好了散伙的打算。

“有个想法我从两年前就开始酝酿,之前做过一个网页版本,现在想法比之前的更加成熟点,我想做成应用。”

我俩在路上走着,边走边聊着。当我听到要转做应用,我有点崩溃掉了。我讨厌应用开发,对应用开发有着相当大抵触的心理。你可能会问我为什么,一个陌生的领域,意味又要开始学习大量的知识,而我还一直幻想着自己仍在游戏开发领域坚持着。你可能会笑了,你作为程序员,不学技术,小心被淘汰!游戏开发中还有很多非常有趣的领域需要探寻,比如游戏人工智能,我舍不得抛下这些。我思想激烈的斗争着,如果转做应用,可能后面我不怎么会接触游戏开发领域方面的东西了,那真是个遗憾的事情。

后面随着聊天的深入,一哥给我介绍了这个app的构想,也就是我现在正在做的项目-《心城》。聊天中途,一哥对于这个应用的某些想法和亮点给了我一些触动,”我很想把这个应用开发出来,让人们都用上她!“我当时心中就萌生了这个决心。

微信、微博、秘密等等这些app占据着社交网络的市场,但我仍然觉得这些并不能管理好我们内心的情感。纷乱嘈杂的过量信息,浮于面子工程的一些文字,并不能让我们活的更好。科技的发展,社交网络的流行拉近了我们的空间距离,却没有拉近我们彼此内心的距离。《心城》,就是我们的的情感管理专家,让我们能更有效去现实社交。情感记录,广场心声,情感数据可视化这些都是我们将要深度挖掘的点。在《心城》,一切围绕着我们关爱的人,以他们为核心来构建应用的整个情感基石。

《心城》应用的价值观和TA未来诞生后的功用性,让我认识到我必须要专心下来学习开发应用,我坚信应用开发并不难。动画的自学,游戏开发的自学,让我对我自己的自学能力抱有信心。为了能让《心城》未来的无与伦比,我必须静下心来做好。

我是否能找到和我一样认同这款产品并当成事业来做的志同道合的伙伴?心城的情感记录特性,会在某个阶段催生TA的赢利点,未来的商业盈利不是问题,我不认为这是现阶段要注意的问题。就如同《知乎》一样,我认为知乎团队在做着非常伟大的事情,虽然这是源自quora的想法。现阶段就是找到对的人,在对的时间,做对的事情。你,是否有兴趣了解?

创业项目:

  • 名称:心城
  • 概念:心城是创新的APP产品,它帮助用户利用移动设备创建一个广义“心上人”的集合,并随时随地记录与这些人有关的情感活动。心诚是我们的的情感管理专家,让我们能更有效去现实社交。你可以暂时将它理解为一个情感方面的通讯录和日记本。心城普遍适合于各种年龄和性别的智能手机用户。我们将基于用户所记录的情感活动推出有针对性的服务,比如撮合互相暗恋者,集体祈福,以及定制礼物等。

目前状况:

  • 产品:概念形态已经定位,产品ios开发进行中
  • 投资:已有若干投资人有投资意向

地点:上海

诚邀伙伴:

  • 技术合伙人(推荐或者自荐,成功则奖励 5000 RMB)
  • IOS开发(推荐或者自荐,成功则奖励一个ipad mini)

详情联系:

路过看这篇博文的朋友,如果你们有这方面的人脉,非常期待能够得到你们的推荐,自荐也欢迎!

也许正是你们不经意的推荐,会给与我们一个坚实的团队,打磨出一个动人的无与伦比的产品!

我们坚信且一路前行!

前言
业余时间在接触 python,从兴趣点着手是个不错的办法。而爬虫正是我感兴趣的一个方向。我根据 younghz 的 Scrapy 教程一步一步来,试着将 Logdown 的博文相关数据抓去下来,作为练手之用,这里做个记录。

工具:Scrapy

Logdown博文相关数据

这里暂时包括如下4个数据:

  • 博文名称 article_name
  • 博文网址 article_url
  • 博文日期 article_time
  • 博文标签 article_tags

Let’s Go!

1. 创建project

命令行cd到某个目录,然后运行如下命令
1
scrapy startproject LogdownBlog

2. items.py的编写

items.py
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
# -*- coding: utf-8 -*-

# Define here the models for your scraped items
#
# See documentation in:
# http://doc.scrapy.org/en/latest/topics/items.html

import scrapy

class LogdownblogspiderItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
article_name = scrapy.Field()
article_url = scrapy.Field()
article_time = scrapy.Field()
article_tags = scrapy.Field()
pass
```
## 3. pipelines.py的编写

```python pipelines.py
# -*- coding: utf-8 -*-

# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: http://doc.scrapy.org/en/latest/topics/item-pipeline.html

import json
import codecs

class LogdownblogspiderPipeline(object):
def __init__(self):
self.file = codecs.open('LogdownBlogArticles.json', mode = 'w', encoding = 'utf-8')

def process_item(self, item, spider):
line = json.dumps(dict(item)) + '\n'
self.file.write(line.decode('unicode_escape'))
return item

将item通过管道输出到文件LogdownBlogArticles.json中,模式为w,用json形式覆盖写入,编码为utf-8编码。

4. settings.py的编写

settings.py
1
2
3
4
5
6
7
8
9
10
BOT_NAME = 'LogdownBlog'

SPIDER_MODULES = ['LogdownBlog.spiders']
NEWSPIDER_MODULE = 'LogdownBlog.spiders'

COOKIES_ENABLED = False

ITEM_PIPELINES = {
'LogdownBlog.pipelines.LogdownblogspiderPipeline':300
}

5. LogdownSpider.py的编写-爬虫分析部分

在 spider 文件夹下新建一个名字为 LogdownSpider.py 的文件,这时目录层次如下:

项目目录层次
1
2
3
4
5
6
7
8
9
10
+ LogdownBlog
| + LogdownBlog
| | + spiders
| | | - __init__.py
| | | - LogdownSpider.py
| | - __init__.py
| | - items.py
| | - pipelines.py
| | - settings.py
| - scrapy.cfg
LogdownSpider.py 爬虫代码
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
# -*- coding: utf-8 -*-

from scrapy.spider import Spider
from scrapy.http import Request
from scrapy.selector import Selector
from LogdownBlog.items import LogdownblogspiderItem

class LogdownSpider(Spider):
'''LogdownSpider'''

name = 'LogdownSpider'

download_delay = 1
allowed_domains = ["childhood.logdown.com"]

first_blog_url = raw_input("请输入您的第一篇博客地址: ")
start_urls = [
first_blog_url
]

def parse(self, response):
sel = Selector(response)
item = LogdownblogspiderItem()

article_name = sel.xpath('//article[@class="post"]/h2/a/text()').extract()[0]
article_url = sel.xpath('//article[@class="post"]/h2/a/@href').extract()[0]
article_time = sel.xpath('//article[@class="post"]/div[@class="meta"]/div[@class="date"]/time/@datetime').extract()[0]
article_tags = sel.xpath('//article[@class="post"]/div[@class="meta"]/div[@class="tags"]/a/text()').extract()

item['article_name'] = article_name.encode('utf-8')
item['article_url'] = article_url.encode('utf-8')
item['article_time'] = article_time.encode('utf-8')
item['article_tags'] = [n.encode('utf-8') for n in article_tags]

yield item

# get next article's url
nextUrl = sel.xpath('//nav[@id="pagenavi"]/a[@class="next"]/@href').extract()[0]
print nextUrl
yield Request(nextUrl, callback=self.parse)

有几个点注意下:

  • xpath 的理解要正确,因为直接关系到我们想要的数据在页面html里的提取。

对 xpath 的分析当然离不了对 html 的分析,我这里采用了 Chrome 浏览器,通过右键-审查元素来查看我们想要的数据在页面中的层次位置。下面以【查看下一篇博文】为例子。

所以通过 xpath 为'//nav[@id="pagenavi"]/a[@class="next"]/@href'就能得到下一篇博文的 url 地址。

  • 设置download_delay,减轻服务器的压力,防止被ban。
  • yield Request(nextUrl, callback=self.parse),获取每个页面的“下一篇博客“的网址返回给引擎,从而循环实现下一个网页的爬取。

6. 执行

1
scrapy crawl LogdownSpider

截图如文章开头图片所示,格式如下:

1
2
3
4
5
...
...
{"article_name": " PhysicsEditorExporter for QuickCocos2dx 使用说明", "article_tags": ["exporter", "Chipmunk", "physicseditor", "quick-x"], "article_url": "http://childhood.logdown.com/posts/196165/physicseditorexporter-for-quickcocos2dx-instructions-for-use", "article_time": "2014-04-28 14:08:00 UTC"}
...
...

相关资料查阅

在命令行操作git push的时候,总是要输入帐号密码,找到了解决方法,做个记录。这里以 github 帐号为例,如果用的 bitbucket 等其他帐号,原理一样。

环境:MacOSX、Linux

步骤

  • .git-credentials```
    1
    -  ```vim .git-credentials```,输入 `https://{username}:{password}@github.com`,username 和 password 分别是你的账户和密码
    -  ```git config --global credential.helper store

完毕后重启终端。