2013年11月27日水曜日

Mac の Skype のデータベースを最適化してパフォーマンスを向上させる小技



もりよしさんにSkypeで教えてもらったのですが、Mac版のSkypeクライアントってデータベースにsqlite3を使用しているんですね。少なくとも3年前には既に知られているネタみたいなのですが、ぜんぜん知りませんでした。
http://d.hatena.ne.jp/shishimaruby/20101214/1292288183

というわけで、Mac版のSkypeをお使いの方は
~/Library/Application Support/Skype/(自分のアカウント名)/
以下にsqlite3のデータベースファイルがあるので、Skypeを一度終了した状態で
sqlite3 main.db VACUUM
sqlite3 main.db REINDEX
という感じでVACUUMREINDEXを実行すると劇的にパフォーマンスが改善します。特に仕事とかで大量のログを見ている方におすすめです。私の場合、370MBもあったログが270MBまで減って、Skypeの起動も高速化しました。

※注意: このコマンドはSkypeのログデータベースを直接操作します。最悪全てのログが失われる危険があります。何を言っているのか分からない、ターミナルの操作なんて知らないという人は実行しないことを強くおすすめします。

Unity の PostprocessBuildPlayer を Ruby で書いてみる(第二版)

UnityでiOSのアプリを作っていて困ることの一つに、iOSが提供するシステムフレームワークへのリンクをプロジェクトに追加するのが超面倒くさいという問題が挙げられます。UnityがiOSアプリを書きだした後、手動でXcode上からシステムフレームワークを追加してもいいのですが、これはとんでもなく面倒です。というわけで、以前こちらの記事でRubyのxcodeprojモジュールを利用して自動的にシステムフレームワークを追加する方法をご紹介しました。
http://akisute.com/2012/09/unity-postprocessbuildplayer-weak.html

今回はそのPostprocessBuildPlayerをさらに機能拡充しましたのでご紹介いたします。主な機能として、
  • システムフレームワークへのリンクをプロジェクトに追加する
    • dylib, framework両方に対応
    • required, optional両方に対応
  • 空のinfo.plistをプロジェクトに追加する
    • ja, enに対応
    • Unity 3時代に空のinfo.plistを追加しないとiOSが提供するUIが英語で表示される問題が合ったため追加
    • Unity 4以上であれば修正されているかも
  • ヘッダサーチパスをライブラリサーチパスからコピーして自動設定する
    • Unity 3時代にビルドにこける事があったので追加
    • Unity 4以上であれば修正されているかも
  • ローンチイメージを自動設定する
    • 容量の関係で極限まで圧縮したjpgをpngの代わりに使いたいということで追加
    • 現在のXcode 5/iOS7向けの環境ではjpgを利用したDefault.pngは全く考慮されていない用に見えるので、使わないほうが無難だと思います
  • main.mmの書き換え
    • ここではSystem.Net.Socket.SocketがSIGPIPEを飛ばしてアプリ全体をクラッシュさせてしまうことがある問題を回避するためにsignalを捕まえたりしています
    • 変更規模が大きいならわざわざここでやるよりUnity側の/Assets/Plugins/iOSにmain.mmを置くほうが良いかと思いますが、ちょっと書き換えるだけなら有用です
  • AppController.mmの書き換え
    • 変更規模が大きいならわざわざここでやるよりUnity側の/Assets/Plugins/iOSにAppController.mmを置くほうが良いかと思いますが、ちょっと書き換えるだけなら有用です
インストール方法は、
  • まずソースコードを取ってきてPostprocessBuildPlayerという名前でUnityプロジェクトの/Assets/Editor/以下に配置します。
  • 実行にはRubyとバージョン0.4.xのxcodeprojモジュールが必要になりますので、インストールします。より大きいバージョンのxcodeprojでは動作未確認ですので、0.4.x系を指定することをおすすめします。
    • sudo gem install xcodeproj --version '~>0.4.0'

ソースコードはこちらになります。MITライセンスです。
#!/usr/bin/env ruby
#
# PostprocessBuildPlayer version 2
# Tested on Ruby 1.8.7, Gem 1.3.6, and xcodeproj 0.4.1
# Created by akisute (http://akisute.com)
# Licensed under The MIT License: http://opensource.org/licenses/mit-license.php
#
require 'rubygems'
# least require version, doesn't work in 0.3.X or less
# possibly work on higher version than 0.4.X, but not tested
gem "xcodeproj", "~>0.4.0"
require 'xcodeproj'
require 'pathname'
require 'fileutils'
#
# Define utility functions
#
def proj_add_system_framework(proj, name) # workaround for 0.4.0 bug of Project#add_system_framework
path = "System/Library/Frameworks/#{name}.framework"
framework_ref = proj.frameworks_group.new_file(path)
framework_ref.name = "#{name}.framework"
framework_ref.source_tree = 'SDKROOT'
framework_ref
end
def proj_add_dylib(proj, name) # added new function, because there's no way we can add dylibs in 0.4.0
path = "usr/lib/#{name}.dylib"
framework_ref = proj.frameworks_group.new_file(path)
framework_ref.name = "#{name}.dylib"
framework_ref.source_tree = 'SDKROOT'
framework_ref
end
def add_system_frameworks_to_project(proj, framework_names, option=:required)
proj.targets.each do |target|
# if target.name == "Unity-iPhone-simulator" then
# next
# end
framework_names.each { |framework_name|
framework = proj_add_system_framework(proj, framework_name) # workaround for 0.4.0 bug of Project#add_system_framework
phase = target.build_phases.find { |phase| phase.is_a?(Xcodeproj::Project::PBXFrameworksBuildPhase) }
ref = phase.add_file_reference(framework)
if option == :optional then
ref.settings = { "ATTRIBUTES" => ["Weak"] }
end
# ref = Xcodeproj::Project::PBXBuildFile.new(proj, nil)
# ref.file_ref = framework
# if option == :optional then
# ref.settings = { "ATTRIBUTES" => ["Weak"] }
# end
# phase = target.build_phases.find { |phase| phase.is_a?(Xcodeproj::Project::PBXFrameworksBuildPhase) }
# phase.files << ref
puts "Added system framework: " + framework_name + " as " + option.id2name
}
end
end
def add_dylibs_to_project(proj, dylib_names, option=:required)
proj.targets.each do |target|
# if target.name == "Unity-iPhone-simulator" then
# next
# end
dylib_names.each { |dylib_name|
dylib = proj_add_dylib(proj, dylib_name)
phase = target.build_phases.find { |phase| phase.is_a?(Xcodeproj::Project::PBXFrameworksBuildPhase) }
ref = phase.add_file_reference(dylib)
if option == :optional then
ref.settings = { "ATTRIBUTES" => ["Weak"] }
end
puts "Added dylib: " + dylib_name + " as " + option.id2name
}
end
end
def new_variant_group_for_group(group, name, sub_group_path=nil)
variant_group = group.project.new(Xcodeproj::Project::PBXVariantGroup)
variant_group.name = name
target = group.find_subpath(sub_group_path, true)
target.children << variant_group
variant_group
end
def add_infoplist_strings_to_project(proj, buildpath)
system("mkdir #{buildpath}/en.lproj")
system("touch #{buildpath}/en.lproj/InfoPlist.strings")
system("mkdir #{buildpath}/ja.lproj")
system("touch #{buildpath}/ja.lproj/InfoPlist.strings")
proj.targets.each do |target|
# if target.name == "Unity-iPhone-simulator" then
# next
# end
infoplist_ref = new_variant_group_for_group(proj.main_group, "InfoPlist.strings")
en_ref = infoplist_ref.new_file("en.lproj/InfoPlist.strings")
en_ref.name = "en"
ja_ref = infoplist_ref.new_file("ja.lproj/InfoPlist.strings")
ja_ref.name = "ja"
buildfile_ref = Xcodeproj::Project::PBXBuildFile.new(proj, nil)
buildfile_ref.file_ref = infoplist_ref
phase = target.build_phases.find { |phase| phase.is_a?(Xcodeproj::Project::PBXResourcesBuildPhase) }
phase.files << buildfile_ref
puts "Added InfoPlist.strings"
end
end
def set_header_search_paths(proj)
proj.targets.each do |target|
# if target.name == "Unity-iPhone-simulator" then
# next
# end
target.build_configuration_list.build_configurations.each do |build_configuration|
header_search_paths = build_configuration.build_settings["LIBRARY_SEARCH_PATHS"]
build_configuration.build_settings["HEADER_SEARCH_PATHS"] = header_search_paths
puts "Added HEADER_SEARCH_PATHS: #{header_search_paths} to build configuration #{build_configuration.name}"
end
end
end
def set_launch_image(proj, unity_project_root_path, buildpath, option)
imagepath = ""
ext = ""
case option
when :png
imagepath = unity_project_root_path + "/Assets/Images"
ext = option.id2name
when :jpg, :jpeg
imagepath = unity_project_root_path + "/Assets/Images/jpg"
ext = option.id2name
system("/usr/libexec/PlistBuddy -c 'Add :UILaunchImageFile string Default.#{ext}' #{buildpath}/Info.plist")
proj.targets.each do |target|
# if target.name == "Unity-iPhone-simulator" then
# next
# end
resource_build_phase = target.build_phases.find { |phase| phase.is_a?(Xcodeproj::Project::PBXResourcesBuildPhase) }
resource_build_phase.files.each do |file|
re = Regexp.new("^(Default.*?)\.png")
if re === file.file_ref.name then
file.file_ref.name = file.file_ref.name.gsub(re, "\\1.#{ext}")
end
if re === file.file_ref.path then
file.file_ref.path = file.file_ref.path.gsub(re, "\\1.#{ext}")
file.file_ref.last_known_file_type = "image.jpeg"
end
end
delete_files = []
sources_build_phase = target.build_phases.find { |phase| phase.is_a?(Xcodeproj::Project::PBXSourcesBuildPhase) }
sources_build_phase.files.each do |file|
re = Regexp.new("^(Default.*?)\.png")
if re === file.file_ref.name then
file.file_ref.name = file.file_ref.name.gsub(re, "\\1.#{ext}")
end
if re === file.file_ref.path then
file.file_ref.path = file.file_ref.path.gsub(re, "\\1.#{ext}")
file.file_ref.last_known_file_type = "image.jpeg"
resource_build_phase.files << file
delete_files << file
end
end
delete_files.each do |file|
sources_build_phase.files.delete(file)
end
end
end
FileUtils.cp("#{imagepath}/Default.#{ext}", "#{buildpath}/Default.#{ext}")
FileUtils.cp("#{imagepath}/Default@2x.#{ext}", "#{buildpath}/Default@2x.#{ext}")
FileUtils.cp("#{imagepath}/Default-568h@2x.#{ext}", "#{buildpath}/Default-568h@2x.#{ext}")
FileUtils.cp("#{imagepath}/Default-Landscape.#{ext}", "#{buildpath}/Default-Landscape.#{ext}")
FileUtils.cp("#{imagepath}/Default-Landscape@2x.#{ext}", "#{buildpath}/Default-Landscape@2x.#{ext}")
FileUtils.cp("#{imagepath}/Default-Portrait.#{ext}", "#{buildpath}/Default-Portrait.#{ext}")
FileUtils.cp("#{imagepath}/Default-Portrait@2x.#{ext}", "#{buildpath}/Default-Portrait@2x.#{ext}")
puts "Added Default images (#{ext}) from #{imagepath}"
end
#
# Define build directory path
# -> Will be suppried as argv if run by Unity
# -> Else, assume unity_project_root_path/build is a build directory
#
unity_project_root_path = File.expand_path(File.dirname($0)) + "/../.."
buildpath = (ARGV[0]) ? ARGV[0] : unity_project_root_path + "/build"
puts "PostprocessBuildPlayer running on build directory: " + buildpath
projpath = buildpath + "/Unity-iPhone.xcodeproj"
proj = Xcodeproj::Project.new(projpath)
###############################################################################
# Example Usages
###############################################################################
#
# Add System frameworks and dynamic libralies to build
#
add_system_frameworks_to_project(proj, ["StoreKit", "Security", "CoreText", "MessageUI"], :required)
add_system_frameworks_to_project(proj, ["Twitter", "Social"], :optional)
add_dylibs_to_project(proj, ["libsqlite3"], :required)
#
# Set HEADER_SEARCH_PATHS
#
set_header_search_paths(proj)
#
# Add UIRequiredDeviceCapabilities to Info.plist
#
system("/usr/libexec/PlistBuddy -c 'Add :UIRequiredDeviceCapabilities: string gyroscope' #{buildpath}/Info.plist")
#
# Update lproj files
#
add_infoplist_strings_to_project(proj, buildpath)
#
# Set Launch Image (Default.png)
#
launch_image_ext = :jpg
set_launch_image(proj, unity_project_root_path, buildpath, launch_image_ext)
#
# Save xcodeproj
#
proj.save_as(projpath)
#
# Rewrite main.mm
# Add uncaught exception handler for better debugging
# Ignores SIGPIPE which rises when application enters background while System.Net.Socket.Socket is open (There's no other way to shut this)
#
f_main_mm = open(buildpath + "/Classes/main.mm", "r")
text_main_mm = f_main_mm.read
f_main_mm.close
f_main_mm = open(buildpath + "/Classes/main.mm", "w")
text_main_mm = text_main_mm.sub(
'int main(int argc, char *argv[])',
'void uncaughtExceptionHandler(NSException *exception);
void uncaughtExceptionHandler(NSException *exception)
{
NSLog(@"%@", exception);
NSLog(@"Stack Trace: %@", [exception callStackSymbols]);
}
int main/*substituted by PostprocessBuildPlayer*/(int argc, char *argv[])'
);
text_main_mm = text_main_mm.sub(
'UnityParseCommandLine(argc, argv);',
'signal(SIGPIPE, SIG_IGN);
NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);
UnityParseCommandLine/*substituted by PostprocessBuildPlayer*/(argc, argv);'
);
f_main_mm.write(text_main_mm)
f_main_mm.close
puts "Updated: main.mm"
#
# Rewrite AppController.mm
# Fixes UIScrollView problems by rewriting run loop modes
# https://github.com/keijiro/unity-ios-textview
#
# Also fixes problems when you use jpg images as a launch image
#
f_app_controller_mm = open(buildpath + "/Classes/AppController.mm", "r")
text_app_controller_mm = f_app_controller_mm.read
f_app_controller_mm.close
f_app_controller_mm = open(buildpath + "/Classes/AppController.mm", "w")
text_app_controller_mm = text_app_controller_mm.sub(
'while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, kInputProcessingTime, TRUE) == kCFRunLoopRunHandledSource)',
'while (CFRunLoopRunInMode(CFStringRef(UITrackingRunLoopMode), kInputProcessingTime, YES) == kCFRunLoopRunHandledSource)'
);
text_app_controller_mm = text_app_controller_mm.sub(
'[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];',
'[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];'
);
text_app_controller_mm = text_app_controller_mm.sub(
'return [NSString stringWithFormat:@"Default%s%s", orientSuffix, szSuffix];',
'return [NSString stringWithFormat:@"Default%s%s.' + launch_image_ext.id2name + '", orientSuffix, szSuffix];'
);
f_app_controller_mm.write(text_app_controller_mm)
f_app_controller_mm.close
puts "Updated: AppController.mm"
puts "PostprocessBuildPlayer completed."
view raw gistfile1.rb hosted with ❤ by GitHub

余談になりますが、最近ではRubyのxcodeprojを使うのではなく、Pythonのmod-pbxprojを使う方法もあるみたいです。@Seasons氏はこちらの方法を使われているそうです。Pythonのがいい!という方はいかがでしょうか。



2013年11月3日日曜日

Objective-Cでパターンマッチしたい

また誰得な妄想ネタですみません><

突然ですが、Objective-Cでパターンマッチがやりたいんです。MLだとかOCamlだとかScalaだとかみたいに。
MLの例: http://kktoppa.web.fc2.com/smlnj4.html
OCamlの例: http://www.geocities.jp/m_hiroi/func/ocaml04.html

パターンマッチがないせいで、StoryboardでSegueを使ったとき、こんな何かの冗談みたいなコードを書かなくちゃいけないんです。
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualsToString:@"Special"]) {
[self doSomething];
} else if ([segue.sourceViewController isKindOfClass:[MyViewController class]] && [segue.destinationViewController isKindOfClass:[MyDetailViewController class]]) {
MyDetailViewController *vc = segue.destinationViewController;
vc.someInfo = self.someInfo;
} else if ([segue.sourceViewController isKindOfClass:[MyViewController class]] && [segue.destinationViewController isKindOfClass:[MyModalViewController class]]) {
MyModalViewController *vc = segue.destinationViewController;
vc.someInfo = self.someInfo;
}
}

見ての通り、大学時代に恩師が「アホのn段重ね」と読んでいたif~elseの重ね技です。これ以外に方法がないんです。

だからこういうコードが書きたいんです。厳密には関数型言語のパターンマッチと違う気がしますしなんか色々とメチャクチャな文法になってますが、だいたいなんかこんな感じのコードが。
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
//
// We NEED this kind of pattern matcher, don't we!? ;(
//
match(@[segue.identifier, segue.sourceViewController, segue.destinationViewController])
{
case @[@"Special", _, _]: ^(_, _, _){
[self doSomething];
}
case @[_, [MyViewController class], [MyDetailViewController class]]: ^(_, _, MyDetailViewController *vc){
vc.someInfo = self.someInfo;
}
case @[_, [MyViewController class], [MyModalViewController class]]: ^(_, _, MyModalViewController *vc){
vc.someInfo = self.someInfo;
}
}
}
view raw PatternMatch.m hosted with ❤ by GitHub

しかし残念ながらObjective-C (CでもC++でもいいですけど) の文法を捻じ曲げるのは極めて難しいです。

Rubyならこんな感じでパターンマッチを実現するライブラリがあるみたいです。素敵ですね。
http://www.callcc.net/diary/20120303.html
https://github.com/k-tsj/pattern-match

というわけで今日もアホのn段重ねでSegueを使おうと思います\(^o^)/