2011年7月17日 星期日

Android 開發筆記 - 設定 Windows 之 Android NDK (Native Development Kit) 開發環境

之前的學習環境都是 Ubuntu 桌機為主,無聊在 ASUS Eee PC 1215N - Intel(R) Atom(TM) CPU D525 / 1.80GHz / 4GB RAM / Windows 7 32-bit 上,安裝 Eclipse 和 ADT 後,發現單獨啟動 Android 2.3.3 模擬器竟然只要 5 分鐘左右耶,這真是神奇了!(這個速度對這類裝備已經算很驚奇了),隨後我便開始著手看看在 Windows 使用 Android NDK 的環境了。


關於 NDK 的使用,採用 Cgywin 環境,如此一來就可以透過 ndk-build 進行編譯的動作了。


安裝項目:



  • Java Platform (JDK) - jdk-6u26-windows-i586.exe (76.8MB)

  • Eclipse Classic - eclipse-SDK-3.7-win32.zip (174MB)

  • Android SDK - installer_r12-windows.exe (34.8MB)

  • Android NDK - android-ndk-r6-windows.zip (64.5MB)

  • Cgywin - 安裝完大小約 225MB


    • 來源選 ftp://ftp.ntu.edu.tw (不行再改用 http://ftp.ntu.edu.tw)

    • 安裝項目:


      • 必要項目 Devel 分類:


        • gcc: C compiler upgrade helper

        • gcc-core: C compiler

        • gcc-g++: C++ compiler

        • gcc-mingw-core: Mingw32 support headers and libraries for GCC

        • gcc-mingw-g++: Mingw32 support headers and libraries for GCC C

        • make: The GNU version of the 'make' utility

        • mingw-runtime: MinGW Runtime



      • 非必要:

        • Net > openssh: The OpenSSH server and client programs

        • Editors > vim: ViIMproved - enhanced vi editor

        • Utils > screen: Run separate screens on a single terminal





    • 小問題:

      • 路徑上有空白的問題

      • 以 Eclipse 的 workspace 來說,預設位置是 C:\Users\username\workspace ,但在 cgywin 上也可以從 c:\Documents and Settings\username\workspace 進入,但從後者來說,路徑上有空白將導致 ndk-build 過程會出錯,請避開使用,若使用者帳號有中文或空白的話,那也盡量把 workspace 移去其他地方吧






上述軟體在 Windows 上的安裝路徑:


C:\Program Files\Java
C:\cygwin
C:\eclipse
C:\Android\android-sdk
C:\Android\android-ndk-r6


隨後在 Eclipse 建立一個 Project,此例為 MyNDK:


Project name: MyNDK
Build Target: Android 2.3.3/Android Open Source Project/2.3.3/10
Application name: MyNDK
Package name: com.example.ndk
Create Activity: MyNDK
Min SDK Version: 3


其他細節請參考 Android 開發教學筆記 - 使用 Android NDK (Native Development Kit) ,除此之外要留意該篇文章的軟體版本或路徑是不一樣的,僅供參考。


透過 Cygwin 測試 ndk-build:


user@pc ~
$ cd /cygdrive/c/Users/username/workspace/MyNDK/jni

user@pc /cygdrive/c/Users/username/workspace/MyNDK/jni
$ ls
Android.mk  my-jni.c

user@pc /cygdrive/c/Users/username/workspace/MyNDK/jni
$ cat Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE    := my-jni
LOCAL_SRC_FILES := my-jni.c
include $(BUILD_SHARED_LIBRARY)

user@pc /cygdrive/c/Users/username/workspace/MyNDK/jni
$ cat my-jni.c
#include <string.h>
#include <jni.h>
jstring Java_com_example_ndk_MyNDK_stringFromJNI( JNIEnv* env, jobject thiz )
{
    return (*env)->NewStringUTF(env, "Hello from My JNI !");
}

user@pc /cygdrive/c/Users/username/workspace/MyNDK/jni
$ cd ..

user@pc /cygdrive/c/Users/username/workspace/MyNDK
$ /cygdrive/c/Android/android-ndk-r6/ndk-build
Compile thumb  : my-jni <= my-jni.c
SharedLibrary  : libmy-jni.so
Install        : libmy-jni.so => libs/armeabi/libmy-jni.so


接著可以設定 Eclipse 讓它可以自動編譯 JNI 的部分,在 Project > Properties > New > Program :


Location: C:\cygwin\bin\bash.exe
Working Directory: ${workspace_loc:${project_path}} (不設定也行,在此沒用到)
Arguments: --login -c "cd /cygdrive/c/Users/username/workspace/${project_path} && /cygdrive/c/Android/android-ndk-r6/ndk-build"


ndk-build-eclipse-cygwin


別忘了設定 Build options,例如 Run the builder 裡勾選 After a clean / During a manual builds 等,就可以透過 clean 來啟動 ndk build 動作。


如此一來,就可以透過 cgywin 裡頭,透過 vim 編輯器撰寫 jni 的程式部分,也可以在裡面呼叫 ndk-build 來測試,最後再切換到 Eclipse 以便測試整合囉。關於這次的 Windows Android NDK 的使用,原先以為多安裝個 cygwin 很麻煩,但仔細想一下我在 Ubuntu 上的使用,一樣也是透過開啟一個 terminal 去編輯 jni 的部分,那轉換在 Windows 上的動作,其實也沒有比較多。


相關文章:



2011年7月14日 星期四

Android 開發筆記 - 使用 NDK / JNI 實作從底層呼叫上層 (C call Java)

一般使用 JNI 的情境,不外呼從 Java 呼叫 C ,接著在 C (底層)運算完後,把數值透過 return 的方式傳回給 Java (上層)端,這在官方 NDK 教學或是之前的筆記都可以看到簡單的範例:Android 開發教學筆記 - 使用 Android NDK (Native Development Kit)。然而,如果要從 Native C 去呼叫 Java function 的話,就不是那麼直觀,例如在 Unix 系統上的程式開發,則是需取得一個門牌號碼(process id),接著才跟他溝通,或是直接透過執行新的程式,去指定運行某個 function 的這種架構等。在此使用的環境為 Ubuntu 10.04 desktop 64-bit,提供簡易 C call Java 的範例。


一般 C call Java 的話,則需要建立一個 Java 運行的 JVM 通道,接著才開始找尋物件、函數並開始運作:


@ c_to_java.c


#include <stdio.h>
#include <stdlib.h>
#include <jni.h>

int main()
{
JNIEnv *env;
JavaVM *jvm;

JavaVMInitArgs vm_args;
JavaVMOption options[1];

// launch Jvm
options[0].optionString = "-Djava.class.path=."; // add user classes
vm_args.version = JNI_VERSION_1_6; //JDK version.
vm_args.options = options;
vm_args.nOptions = 1;

if( JNI_CreateJavaVM(&jvm, (void*) &env, &vm_args) < 0 )
{
fprintf( stderr , "Launch JVM Error\n" );
exit(1);
}

// find the obj & method
jclass my_class;
jmethodID my_main;

if( !( my_class = (*env)->FindClass( env, "MyJavaClass") ) )
{
fprintf( stderr , "'Class' Not Found\n" );
exit(1);
}

if( !( my_main = (*env)->GetStaticMethodID( env, my_class , "main" ,  "([Ljava/lang/String;)V" ) ) )
fprintf( stderr , "'main' Not Found\n" );
else// Call main function
(*env)->CallStaticVoidMethod( env, my_class, my_main, NULL);

// finish
(*jvm)->DestroyJavaVM(jvm);

return 0;
}


@ MyJavaClass.java


class MyJavaClass
{
public static void main( String []arg)
{
System.out.println("Hello World");
}
}


@ Makefile


CC=gcc
INCLUDE=-I/usr/lib/jvm/java-6-sun/include/ -I/usr/lib/jvm/java-6-sun/include/linux/
LIB=/usr/lib/jvm/java-6-sun/jre/lib/amd64/server/libjvm.so

all:
        javac MyJavaClass.java
        $(CC) $(INCLUDE) $(LIB) c_to_java.c     
clean:
        rm -f ./a.out  ./*.class


@  /etc/ld.so.conf.d/jvm.conf 


/usr/lib/jvm/java-6-sun/jre/lib
/usr/lib/jvm/java-6-sun/jre/lib/amd64
/usr/lib/jvm/java-6-sun/jre/lib/amd64/server


執行結果:


$ make
$ ./a.out 
Hello World


由於我是在 Ubuntu 10.04 desktop 64-bit 環境,安裝 Sun Java6,所以 gcc 編譯要透過 -I 指定 header file 和 libjvm.so 檔案外,在執行 a.out 的時候也會出現找不到 share library 的情,這時候要去設定 /etc/ld.so.conf,在此是透過建立 /etc/ld.so.conf.d/jvm.conf 並再用 sudo ldconfig -v 進行 loading ,之後在執行 a.out 就不會出錯了


接著是 Android 端之 JNI 從底層 C 呼叫上層 Java 端的部分,稍稍不一樣,整個程式運作流程:



  1. 啟動 Android app 時,進入程式內的 OnCreate 時,將本身 app 的資訊丟進 C 底層,並使用全域變數紀錄。

  2. 由於 C 帶有 Java 端相關資訊,因此 C 可以呼叫 Java 端的相關函數,完成了底層往上層呼叫的動作。


建構於Android 開發教學筆記 - 使用 Android NDK (Native Development Kit) 的範例:


請留意 NDK 版本,此例是用 android-ndk-r5c ,用新版的話,相關路徑要記得更新


目錄結構:


~/android-ndk-r5c
~/workspace/MyNDK
~/workspace/MyNDK/jni
~/workspace/MyNDK/jni/my-jni.c
~/workspace/MyNDK/jni/Android.mk
~/workspace/MyNDK/jni/Makefile  (非必要)


@ MyNDK.java


package com.example.ndk;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;
import java.lang.ref.WeakReference;

public class MyNDK extends Activity {
    /** Called when the activity is first created. */
static int count;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setContentView(R.layout.main);
        TextView tv = new TextView(this);
        tv.setText( stringFromJNI() );
        setContentView(tv);
        count = 0;
       
        initMyJNI(new WeakReference<MyNDK>(this));
    }

    private static void callback( int pid )
    {
        Log.i("MyNDK","Pid:"+pid+",Count:"+count);
    count++;
    }


    private native void initMyJNI( Object weak_this );

    public native String  stringFromJNI();

    static {
        System.loadLibrary("my-jni");
    }

}


@ my_jni.c


#include <stdio.h>
#include <string.h>
#include <jni.h>
#include <pthread.h>
#include <android/log.h>

#define LOG_TAG"MyJNI"
#define LOGV(...)__android_log_print( ANDROID_LOG_VERBOSE,LOG_TAG, __VA_ARGS__ )
#define LOGD(...)__android_log_print( ANDROID_LOG_DEBUG,LOG_TAG, __VA_ARGS__ )
#define LOGI(...)__android_log_print( ANDROID_LOG_INFO,LOG_TAG, __VA_ARGS__ )
#define LOGW(...)__android_log_print( ANDROID_LOG_WARN,LOG_TAG, __VA_ARGS__ )
#define LOGE(...)__android_log_print( ANDROID_LOG_ERROR,LOG_TAG, __VA_ARGS__ )

// --- Header Begin ---
#defineJAVA_CALLBACK_FUNCTION_NAME"callback"
#defineJAVA_CALLBACK_FUNCTION_ARGS_TYPE"(I)V"

JNIEXPORT void JNICALL Java_com_example_ndk_MyNDK_initMyJNI( JNIEnv* env, jobject thiz, jobject weak_this );
void * threadJobRun();

static const char *classPathName = "com/example/ndk/MyNDK";
static JNINativeMethod methods[] = {
{
"initMyJNI" , 
"(Ljava/lang/Object;)V" , 
(void *)Java_com_example_ndk_MyNDK_initMyJNI

};

static JavaVM *jvm;// global for JVM init Info
static jobject mObject;// get the object
static jclass mClass;// get the class
static jmethodID mMethodID;// get the method

static pthread_t thread;

// --- End Of Header ---


static int registerNativeMethods(JNIEnv* env, const char* className, JNINativeMethod* gMethods, int numMethods)
{
jclass clazz;

if( !( clazz = (*env)->FindClass( env, className ) ) )
{
LOGE( "Unable to get the Class at registerNativeMethods" );
return JNI_FALSE;
}

if( ( (*env)->RegisterNatives( env, clazz, gMethods, numMethods ) ) < 0 )
{
LOGE( "Unable to register the methods at registerNativeMethods" );
return JNI_FALSE;
}
return JNI_TRUE;
}

jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
LOGI("IN JNI_OnLoad");
JNIEnv *env;
if( ( (*vm)->GetEnv( vm, (void **)&env, JNI_VERSION_1_6 ) ) != JNI_OK )
{
LOGE( "Unable to get the env at JNI_OnLoad" );
return -1;
}
if (!registerNativeMethods( env, classPathName, methods, sizeof(methods) / sizeof(methods[0])))
{
LOGE( "Unable to register native methods at JNI_OnLoad" );
return -1;
}
jvm = vm;
return JNI_VERSION_1_6;
}

JNIEXPORT void JNICALL Java_com_example_ndk_MyNDK_initMyJNI( JNIEnv* env, jobject thiz, jobject weak_this )
{
jclass clazz;

mClass = NULL;
mObject = NULL;
mMethodID = NULL;

if( !( clazz = (*env)->GetObjectClass( env, thiz ) ) )
{
LOGE( "Unable to get the object class" );
return;
}
if( !( mClass = (*env)->NewGlobalRef( env, clazz ) ) )
{
LOGE( "Unable to get the class ref" );
return;
}
if( !( mObject = (*env)->NewGlobalRef( env, weak_this ) ) )
{
LOGE( "Unable to get the object ref" );
return;
}

if( !( mMethodID = (*env)->GetStaticMethodID( env, mClass, JAVA_CALLBACK_FUNCTION_NAME, JAVA_CALLBACK_FUNCTION_ARGS_TYPE ) ) )
{
LOGE( "Unable to get the method ref" );
return;
}

LOGI("IN initMyJNI");

if( mClass && mMethodID )// first call
(*env)->CallStaticVoidMethod( env, mClass , mMethodID , (int)getpid() );

// thread
pthread_create( &thread, NULL, threadJobRun, NULL);
}
void * threadJobRun()
{
JNIEnv *env;
int isAttached;

isAttached = 0;
env = NULL;
LOGI("IN threadJobRun");
if( jvm )
{
LOGI("IN threadJobRun with JVM");

if( ( (*jvm)->GetEnv( jvm, (void**) &env, JNI_VERSION_1_6 ) ) < 0 )
{
LOGI( "Unable to get env at threadJobRun" );
if( ( (*jvm)->AttachCurrentThread( jvm, &env, NULL) ) < 0 )
{
LOGE( "Unalbe to attach current thread at threadJobRun" );
return NULL;
}
isAttached = 1;
}

while( 1 )
{
sleep(1);

// --- jobs begin
if( mClass && mMethodID )
{
LOGI( "call mClass & mMehtodID" );
(*env)->CallStaticVoidMethod( env, mClass , mMethodID , (int)getpid() );
}
// --- end of jobs
}

if( isAttached )
{
(*jvm)->DetachCurrentThread( jvm );
}
}
return NULL;
}

jstring Java_com_example_ndk_MyNDK_stringFromJNI( JNIEnv* env, jobject thiz )
{
    return (*env)->NewStringUTF(env, "Hello from My JNI !");
}


@ Android.mk


LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := my-jni
LOCAL_SRC_FILES := my-jni.c

LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog

include $(BUILD_SHARED_LIBRARY)


@ Makefile (非必要)


all:
cd ../ && sh ~/android-ndk-r5c/ndk-build


其中 Makefile 是我自己工作上使用的,我習慣用 vim + makefile 開發,所以會需要這個檔案,所以此檔非必要,其他檔案如同 Android 開發教學筆記 - 使用 Android NDK (Native Development Kit) 敘述。比較重要的是 my-jni.c 裡頭,有一個 JNI_OnLoad 和 initMyJNI 函數,在 app 一開始執行後,會先透過 JNI_OnLoad 將相關的資訊紀錄在 global variables 中,如 JVM 以及相關動作等,接著在 Java 呼叫 initMyJNI 時,才正式將 Java Object 紀錄起來,例如 Class 、Method 等,另外,在 initMyJNI 時,已經算是做到 C call Java 之 callback 的動作了(first call),只是我的目標是在 C 層跑一個 thread ,等到有事件時去更新 Java 層,所以有多一個 thread 的運行,在 threadJobRun 中則是 thread 的動作囉。


程式運作過程中,我有透過 Logcat 印出點訊息,可以看得清楚:


DDMS_MyJNI


其中 tag 為 MyJNI 是指 C 那層的動作,可以看到流程是 IN JNI_OnLoad、IN initMyJNI,接著就是第一次 c call java,在 Java 層印出 Pid:853, Count:0 的訊息,接著才進入 Thread ,然後每秒印出一次 Pid:853, Count:number 等資訊。


其他比較重要的是 C 裡頭也能可以呼叫 logcat ,記得在 Android.mk 中要加入 LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog 才不會編譯失敗(擺放的位置也很重要,擺在上頭會失敗);而 c call java 的部分,在 Java 那邊定義的函數必須是 static 的,此例為 private static void callback( int pid ),不然也會出錯(因為 C 裡頭是用 GetStaticMethodID ),我有去找一下其他 open source (Kwaak3 / KwaakJNI.java),寫法也是這樣。


最後一提,上述 C 程式存在很多地方沒寫好,例如資源的釋放等,在此僅供用來了解流程用途。


參考資料:



2011年7月13日 星期三

iOS 開發教學 - 關於 Crash Report 和 dSYM 的使用

前幾天玩遊戲時,看到開發者請大家把 Crash Log / Crash Report / Bug Report 寄回給他,如此一來就可以偵錯,滿好奇的便也隨意在網路上找找該怎樣使用,在此便稍作筆記,並且寫一個簡單會 crash 的程式(很少人會這樣做吧 XD)。

所謂的 Crash Report 是在實體機器上,執行程式時發生 crash 時,此時 iOS 會幫忙將程式出錯的相關資訊存起來,這時候用 Xcode 的 Organizer - Devices 時,可以去點選設備,並且可以查看有哪些 report 在上頭,這時候就可以稍作觀看,但是上頭記錄的資訊是有稍微類似做過保護的,只會看到類似(這是網路上收集的資訊,但我嘗試做出的 log 好像已經轉好了?):

Thread 0 Crashed:
0   libSystem.B.dylib              0x35de3ad0 0x######## + ##
1   libSystem.B.dylib              0x35de3abe 0x######## + ##
2   libSystem.B.dylib              0x35de3ab2 0x######## + ##
3   libSystem.B.dylib              0x35dfad5e 0x######## + ##
4   libSystem.B.dylib              0x35de967e 0x######## + ##
5   libSystem.B.dylib              0x35de974c 0x######## + ##
6   libSystem.B.dylib              0x35d5c8e0 0x######## + ##
7   libSystem.B.dylib              0x35d5c798 0x######## + ##
8   CoreFoundation                 0x3750022c 0x######## + ##
9   CoreFoundation                 0x374fc206 0x######## + ##
10  CoreFoundation                 0x37504564 0x######## + ##
11  CoreFoundation                 0x37504406 0x######## + ##
12  Foundation                     0x351530f0 0x######## + ##
13  CrashTesting                   0x00002712 0x######## + ##
14  UIKit                          0x35924fd0 0x######## + ##
15  UIKit                          0x3591ea70 0x######## + ##
16  UIKit                          0x358d82dc 0x######## + ##
17  UIKit                          0x358d7b14 0x######## + ##
18  UIKit                          0x358d73ac 0x######## + ##
19  GraphicsServices               0x33e77c80 0x######## + ##
20  CoreFoundation                 0x3752f5c4 0x######## + ##
21  CoreFoundation                 0x3752f582 0x######## + ##
22  CoreFoundation                 0x3752182e 0x######## + ##
23  CoreFoundation                 0x37521504 0x######## + ##
24  CoreFoundation                 0x37521412 0x######## + ##
25  UIKit                          0x3591d54c 0x######## + ##
26  UIKit                          0x3591a550 0x######## + ##
27  CrashTesting                   0x0000266e 0x######## + ##
28  CrashTesting                   0x00002620 0x######## + ##

這時候需要使用原先編譯 app 時,在同一目錄中會產生對應的 dSYM 檔案,搭配 dwarfdump 使用才可以查詢到真正的片段程式(例如想看 line 13 的意思):

$ dwarfdump --lookup 0x00002712 -arch armv6 CrashTesting.app.dSYM/
----------------------------------------------------------------------
 File: CrashTesting.app.dSYM/Contents/Resources/DWARF/CrashTesting (armv6)
----------------------------------------------------------------------
Looking up address: 0x0000000000002712 in .debug_info... found!

0x00000132: Compile Unit: length = 0x000049b0  version = 0x0002  abbr_offset = 0x00000000  addr_size = 0x04  (next CU at 0x00004ae6)

0x0000013d: TAG_compile_unit [1] *
             AT_producer( "4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2373.6)" )
             AT_language( DW_LANG_ObjC )
             AT_name( "CrashTestingAppDelegate.m" )
             AT_entry_pc( 0x000026b4 )
             AT_stmt_list( 0x0000006b )
             AT_comp_dir( "/Volumes/Data/iPhoneDev/trunk/CrashTesting/CrashTesting" )
             AT_APPLE_major_runtime_vers( 0x02 )

0x00000166:     TAG_subprogram [16] *
                 AT_sibling( {0x000001d2} )
                 AT_name( "-[CrashTestingAppDelegate application:didFinishLaunchingWithOptions:]" )
                 AT_decl_file( "/Volumes/Data/iPhoneDev/trunk/CrashTesting/CrashTesting/CrashTestingAppDelegate.m" )
                 AT_decl_line( 17 )
                 AT_prototyped( 0x01 )
                 AT_type( {0x0000015b} ( BOOL ) )
                 AT_APPLE_isa( 0x01 )
                 AT_low_pc( 0x000026b4 )
                 AT_high_pc( 0x00002780 )
                 AT_frame_base( r7 )

0x000001b9:         TAG_lexical_block [5] *
                     AT_low_pc( 0x000026c4 )
                     AT_high_pc( 0x00002780 )
Line table file: 'CrashTestingAppDelegate.m' line 20, column 0 with start address 0x000000000000270e

Looking up address: 0x0000000000002712 in .debug_frame... found!

0x00000020: FDE
        length: 0x0000000c
   CIE_pointer: 0x00000000
    start_addr: 0x000026b4 -[CrashTestingAppDelegate application:didFinishLaunchingWithOptions:]
    range_size: 0x000000cc (end_addr = 0x00002780)
  Instructions: 0x000026b4: CFA=4294967295+4294967295

因此就可以查看到 line 13 是指 CrashTestingAppDelegate.m' line 20,所以就可以去那邊追查看看。

以上的例子僅需建立一個 Project (此例名為 CrashTesting),然後在 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 裡頭寫一段會 crash 的程式碼(此例為 double free):


    NSString *justCrash = [[NSString alloc] initWithFormat:@"CrashTesting"];
    [justCrash release];
    [justCrash release];

如此一來,在實機執行完並當機後,可以接上 Xcode 去 Organizer - Devices 查看:

xcode-crash-log

而 dSYM 檔案,則是在 Xcode 編譯的地方:

xcode-dSYM

點選後,就可以看到並且可以複製到習慣的工作目錄囉

xcode-app-dSYM

參考資料:


iPhone/iPad 遊戲 - 大家來搶錢 HD

大家來搶錢 HD-0 大家來搶錢 HD-1


前陣子趁著限時免費時,下載了這款遊戲(現在要價 1.99 美金)。後來過了一陣子後,同事一直在玩也推薦我玩,就不小心也陷入想要把地圖全破的慾念中。這款就像是兒時 PC 上的大富翁遊戲,可以選地圖、餐與人物,遊戲過程中藉由買土地、建房子、收過路費等模式進行,除此之外,有簡單的卡片系統,可以買特殊的卡片,如停留卡、購地卡、請神卡、均富卡等等,當然還有四個神仙供你參拜以及錢滾錢的股票系統。


大家來搶錢 HD-2


目前共提供 10 個地圖,玩起來還不錯,就跟 PC 上的大富翁很像,只是沒那麼複雜。這幾天開始試試股票系統,今天不小心就從 4 塊錢買進,遊戲進行中又使用了幾張紅卡後,目前市值已經有 99 元,使得 UI 顯示已經爆表看不到股票現值了 XD 然後定期會有股票現金股利吧?不知不覺手頭上的現金也蹦出一堆。我想,我應該可以正式從這款遊戲畢業了 XDD


大家來搶錢 HD-3


這款應該是香港或大陸人開發的遊戲吧?我覺得品質還不錯,隨著地圖有對應的風格背景、遊戲聲效等,最近好像有更新軟體?或許修正了一些問題或增加難度了吧。雖然我對遊戲開發還沒有熱情,但透過這款遊戲的刺激,哪天也來研究一下遊戲吧!總覺得這款是透過某種機制 porting 到 iPhone 上頭,如果也是走 OpenGL ES 的話,或許 Android 也能無痛 porting 呢。


最後一提,這款有支援 iPad 喔,算挺不賴的!


2011年7月11日 星期一

iPhone 開發教學 - A Simple Socket Server Example


來源:Introduction to CFNetwork Programming Guide


前陣子用了一些 iPhone app,其中有些 app 有提供小型的 web service,因此讓我想練習 iOS socket programming,反正暑假也到了,我就抽點時間東看西看。


花了不少時間,才擠出一點小範例。在 iOS 上寫 socket server 的方式,跟一般工作站寫的不一樣,以前在工作站寫 server 時,因為一開始執行並不會有 client 馬上連線,所以要將 server 程式用 loop 等著,以免程式終止。在 iOS 上寫 socket server 則有點像 Web 上寫 AJAX 的模式,改成 EVENT-DRIVEN 架構,變成要註冊 client 連線事件、client 傳輸資料、client 終止連線等相關事件。這些大概是跟 UI 操作架構有關,因此在 iOS 上寫 socket server 時,不需再用 loop 架構等在那邊(若用 loop 等在那邊也容易出錯)。


建立 iOS socket server 流程如下:



  1. 初始化 socket server 資訊,如使用的 IP 和 Port number

  2. 設定 socket accept 事件處理

  3. 設定 socket connect 事件處理

  4. 設定 socket close 事件處理


雖然 iOS 支援 BSD Socket,只是還是用 BSD Socket 練習,那就會沒學到新東西,所以此處採用稍微上層的 Core Foundation/CFNetwork 架構,物件上採用 CFSocket,此例是在 iPhone Simulator 上運行一支 socket server ,然後透過 cmd line 的 telnet 進行連線測試。僅須把程式碼都寫在 YourProjectAppDelegate.m 之裡頭即可(如:SocketServer)。


AddCFNetworkLib
記得要新增 CFNetwork framework


程式碼:


#import <CFNetwork/CFNetwork.h>
#import <arpa/inet.h>
#import <sys/socket.h>
#import <netinet/in.h>
#import <arpa/inet.h>
#import <netdb.h>

// Global variables 
CFSocketRef server;
int serverPort = 5566;
CFMutableDictionaryRef incomingRequests;
NSFileHandle *listeningHandle;


- (void)clientHandleClose:(NSFileHandle *)incomingFileHandle close:(BOOL)closeFileHandle
{
NSLog(@"socket close");
if (closeFileHandle)
{
[incomingFileHandle closeFile];
}

[[NSNotificationCenter defaultCenter] removeObserver:self name:NSFileHandleDataAvailableNotification object:incomingFileHandle];
CFDictionaryRemoveValue(incomingRequests, incomingFileHandle);

}

- (void)clientDataReceiveNotification:(NSNotification *)notification
{
NSLog(@"receive data");
NSFileHandle *incomingFileHandle = [notification object];
NSData *data = [incomingFileHandle availableData];

if ([data length] == 0)
{
[self clientHandleClose:incomingFileHandle close:NO];
return;
}
NSString *formatedData = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
NSLog(@": %@", formatedData);
[formatedData release];
    
[self clientHandleClose:incomingFileHandle close:YES];
}

- (void)clientAcceptNotification:(NSNotification *)notification
{
NSLog(@"server accept");
    
NSDictionary *userInfo = [notification userInfo];
NSFileHandle *incomingFileHandle = [userInfo objectForKey:NSFileHandleNotificationFileHandleItem];
    
if(incomingFileHandle)
{
CFDictionaryAddValue( incomingRequests, incomingFileHandle, [(id)CFHTTPMessageCreateEmpty(kCFAllocatorDefault, TRUE) autorelease]);

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(clientDataReceiveNotification:) name:NSFileHandleDataAvailableNotification object:incomingFileHandle];
[incomingFileHandle waitForDataInBackgroundAndNotify];
}

    
[listeningHandle acceptConnectionInBackgroundAndNotify];
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
int socketSetupContinue = 1;
struct sockaddr_in addr;
    
if( !(server = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_STREAM,IPPROTO_TCP, 0, NULL, NULL) ) )
{
NSLog(@"CFSocketCreate failed");
socketSetupContinue = 0;
}
    
if( socketSetupContinue )
{
int yes = 1;
if( setsockopt(CFSocketGetNative(server), SOL_SOCKET, SO_REUSEADDR, (void *)&yes, sizeof(int)) )
{
NSLog(@"setsockopt failed");
CFRelease(server);
socketSetupContinue = 0;
}
}
    
if( socketSetupContinue )
{
memset(&addr, 0, sizeof(addr));
addr.sin_len = sizeof(struct sockaddr_in);
addr.sin_family = AF_INET;
addr.sin_port = htons(serverPort);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
        
//CFDataRef *address = CFDataCreate(NULL, (const UInt8 *)&addr, sizeof(addr));
//[(id)address autorelease];
//if (CFSocketSetAddress(server, address) != kCFSocketSuccess) 
NSData *address = [NSData dataWithBytes:&addr length:sizeof(addr)];
if (CFSocketSetAddress(server, (CFDataRef)address) != kCFSocketSuccess) 
{
NSLog(@"CFSocketSetAddress failed");
CFRelease(server);
socketSetupContinue =0;
}
}
    
if( socketSetupContinue )
{
incomingRequests = CFDictionaryCreateMutable( kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
listeningHandle = [[NSFileHandle alloc] initWithFileDescriptor:CFSocketGetNative(server) closeOnDealloc:YES];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(clientAcceptNotification:) name:NSFileHandleConnectionAcceptedNotification object:nil];
[listeningHandle acceptConnectionInBackgroundAndNotify];

        
NSLog(@"Socket listening on %s:%d", addr2ascii(AF_INET, &(addr.sin_addr.s_addr),sizeof(addr.sin_addr.s_addr),NULL), serverPort);
}
    
// Override point for customization after application launch.
// Add the navigation controller's view to the window and display.
self.window.rootViewController = self.navigationController;
[self.window makeKeyAndVisible];
return YES;
}


模擬器一開啟後,就會聽在 5566 port 上頭,接著可以用 telnet localhost 進行測試:


$ telnet localhost 5566
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.


一打完就可以看到 iPhone Simulator 印出 server accept 字樣


 Socket listening on 0.0.0.0:5566
 server accept


接著隨意打一些字串,就可以看到 Simulator 印出接收到的字串以及把連線中斷的訊息,當然 client 也就中斷了。


上述粗體字是全域變數,可以加在 @interface 裡頭或是之接擺在 @implementation 後面也行。藍色部分是 Event 的設定,由於 socket handle 在系統底層也算是個 file handle,所以事件的偵測就跟偵測 file 變化類似,例如檔案開檔、新增資料和關檔等等。


參考資料: