C# Winform으로 만든 간단한 타이머입니다.


이 장난감(?)은 누구든지 자유롭게 디컴파일링, 분해, 구축이 가능합니다. 편히 쓰세요.



please.zip




* 사용법


- 딱히 없습니다. 프로그램을 실행하고, 퇴근시간 설정 버튼으로 시간을 설정 후 어디 구석에 놔두시면 됩니다. 

그리고 멍하니 처다봅니다.


- 작업표시줄에 표시되지 않습니다.


- 기본적으로 시작프로그램에 등록이 되게 됩니다. 시작시 실행을 원치 않으신다면 꼭 해제해주세요.


- 출근시간 전에 이 프로그램을 실행하셨다면 아주 성실하신 분이므로 칭찬합니다. 출근시간까지 n시간 남았다고 표시합니다.


- 퇴근시간을 넘어가면 자동으로 야근모드로 바뀝니다.


- 시간설정에 기본적으로 24시계를 적용합니다. 저녁 6시라면 18시로 입력하셔야 합니다.


- 프로그램이 계속 실행되고 있고 자정을 넘어서도 철야로 야근을 하고 계신게 아니라면, 시간을 다시 리셋하셔야 합니다.

등대 여러분들은 조속히 퇴근해주시길 바랍니다.


- 반드시 timeData.dat 파일이 같은 장소에 있어야합니다. 출퇴근시간 및 코멘트를 BinaryFormat 형태로 저장합니다.







디버깅이란 항상 그렇듯 수시간 또는 수일을 고민해서 결국 해결봤더니 별 거 아니었더라.. 하는 경우가 많은데


이번에도 정말 어김없이, 그랬다. 두 번 겪고싶지는 않아서 기록해 둠.



이번 이슈의 개요는,


<MissingReferenceException>



Device Camera를 이용한 AR 프로젝트 빌딩 중, 여러 컨텐츠를 왔다갔다 실행하며 저 Exception Error을 띄웠다.


StackTracing을 해봤더니 전혀 엉뚱한 곳을 가리키고 있길래 아! 요고요고 그거다, eventHandler를 제대로 관리하지 않았구나, 하고 감이 왔었다.


<SceneLoad 관련 Action 추가함수>



간단한 디버깅 결과, 저 Action에서 null참조를 가리킨다는 것을 알았고(사실 여기까지 멍청하게 한시간 이상 걸림) 원인 파악에 나섰다.


+ 연산으로 Event Handler에 등록된 게임 오브젝트가 씬 전환 등으로 삭제가 되면


이 레퍼런스는 당연히 없는(null) 값을 가리키게 될 것이고, 그 값 또한 GC에 의해 null로 셋팅된다. 즉 헛손질을 하게 된다..는 것이었다.


그 후, 다시 + 연산이 호출될 때 null 값으로 정의된 레퍼런스는 그대로 유지된 채 Event가 붙어버려 Missing 오류를 띄우는 것.


객체를 두 개 이상 생성하는 것도 멍청했고, 이 Event의 참조 또한 객체 수만큼 늘어나는게 원초적인 문제라면 문제였다.



원래는 위 주석처리된 함수처럼 람다식을 이용해 event를 붙였지만, 객체가 생성과 파괴를 반복할 수도 있다는 것을 알았으니


함수형으로 만들고, 해당 객체가 파괴되기 전에 Action을 빼 주는 코드를 추가했다.




<자, 이제 빼 주자.>



객체가(여기서는 AR관련 스크립트를 포함한 객체) 비활성화 될때 해당 Action을 빼 준다. 해결!! 하면서 다시 테스트를 해 보았지만..


안된다.(2차빡침)


왜? 대체 왜? 분명히 Event까지 잘 빼주고, 객체가 다중생성 되는것, 그리고 GC와 Resource관련 패널도 정확히 검사했는데 대체 왜..?


그리고 이 버그가 특정 Scene의 이동시에만 발생하는 버그가 StackTracing을 더 어렵게 만들었다.


아무리 생각해도 이 Action에서 문제를 일으키는건 응당해보여서 다시 Test를 해봤더니..


OnDisable()이 호출 자체가 안 된다.




-_-?? 그럴 수 있나?


코드를 보면 알겠지만 해당 액션은 SceneManager의 sceneLoaded에 관한 것이다. 하지만 해당 스크립트를 사용하는 씬은 Additive모드로 가져온 것.



<해당 Scene의 Hierarchy 구조>


현재 ARCamera Scene이 Additive 모드로 불려져 있고, 여기서 네트워크 신호(Exit Response)를 받으면 다른 Scene이 AsyncLoad가 되는 구조였다.


즉, 본디 실행중인 메인 Scene에서 비동기 씬로드가 이루어지면 Additive Scene에 포함된 객체에서 OnDisable()이 호출이 되지 않는다는 것.



호오.......



응, 그래서 어떻게든 객체가 파괴되는 시점은 분명히 올 것이라 생각하고 OnDestroy에 넣었더니


정상적으로 Event를 잘 빼주게 되더라. 여기서 1차 환호. 디버깅은 완료했다.





사실 Event Handler에 등록한 그 Action은 단순히 Device Camera를 찾아 셋팅하는 기능 밖엔 없었다 ^ u^;; 결국 1회용 함수라 생각해서


카메라를 끌 때 빼는것도, 파괴될때 빼는것도 아닌 그냥.. 그냥 넣자마자 빼면 단순히 해결이 되는 문제였다 - _-;


분명 며칠전까지만 해도 아주 잘 되던 유니티 Apk빌드가 갑자기 말썽을 일으켰습니다.


Git branch를 잘못 따라간 것도 아니었고... 분명 빌드 설정이나 셋팅에 관해선 아~~무것도 만진 것이 없었는게 갑자기 말이에요.


제가 했던 진짜 수많은 삽질은 좀 뒤로 하고, 문제 해결에 도움이 되었던 결정적인 방향들만 기록해둡니다.







일단 발단은 이렇습니다. 잘 되던 유니티 빌드가 안 되고, 로그를 추적해 보니..





CommandInvokationFailure: Unable to merge android manifests. 

C:/Program Files/Java/jdk1.8.0_221\bin\java.exe -Xmx4096M -Dcom.android.sdkmanager.toolsdir="C:\Users\user\AppData\Local\Android\sdk\tools" -Dfile.encoding=UTF8 -jar "C:\Program Files\Unity\Editor\Data\PlaybackEngines\AndroidPlayer/Tools\sdktools.jar" -


stderr[

Error: [Temp\StagingArea\AndroidManifest-main.xml:4, C:\Project\01.Client\Temp\StagingArea\android-libraries\AndroidPlugin\AndroidManifest.xml:16] Trying to merge incompatible /manifest/application/activity[@name=kr.co.d.MainActivity] element:

  <activity

--    @android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale|layoutDirection|density"

--    @android:hardwareAccelerated="false"

--    @android:label="@string/app_name"

  <activity

++    @android:name="kr.coid.MainActivity">

++    <intent-filter>

++        <action

]

stdout[

[Temp\StagingArea\AndroidManifest-main.xml:18, C:\Project\CotrasHybrid\01.Client\Temp\StagingArea\android-libraries\AndroidPlugin\AndroidManifest.xml:29] Skipping identical /manifest/application/receiver[@name=kr.co.netblue.cotrashybrid.BootBroadcastReceiver] element.

]

exit code: 1




Unable to merge android manifests.


안드로이드 쪽에서 빌드하며 Manifest.xml 충돌이 일어난 듯 합니다. 그런데 딱히 json쪽에서 파싱한다거나 따로 건든 적도 없고 했는데..

저 "@android:name=" 부분이 중복이 되었다 판단해서 해당 구문을 삭제했습니다. (진짜 희한한건 아무도 이 Manifest를 만진 사람이 없다는 건데..)


어쨌든, Manifest 충돌을 해결하고 SDK 버전을 조정한 뒤에, Gradle 빌드에 성공했으나.. apk파일이 깨져서 나옵니다. 디바이스에서 파싱을 못 해요.



그래서 다시 Internal 빌드로 눈길을 돌립니다. 사실 이제것 Gradle 빌드를 쓰지 않았었거든요. 유니티 버전을 19로 올리면 Gradle 빌드밖에

안 된다고는 하는데... ㅠㅠ 잘 모르겠습니다 일단은..


Manifest 병합에는 일단 성공을 했고, 두 번째 문제를 마주쳤습니다.






CommandInvokationFailure: Failed to align APK.

C:\Users\user\AppData\Local\Android\sdk\build-tools\29.0.2\zipalign.exe 4 "C:\Project\CotrasHybrid\01.Client\Temp/StagingArea\Package_unaligned.apk" "C:\Project\CotrasHybrid\01.Client\Temp/StagingArea\Package.apk"


stderr[

Unable to open 'C:\Project\CotrasHybrid\01.Client\Temp/StagingArea\Package_unaligned.apk' as zip archive

]

stdout[


]

exit code: 1



Failed to align APK.


찾아보니 Unalign된 apk 둘을 최종적으로 align하게 만드는 과정에서... 빌드 에러가 난 것 같아요


그런데 이를 특정하기 전에 저 오류코드는 실로 너무너무너무너무 많은 사람들이 겪던 이슈라 해결방법이 각자 다 달랐어요. 일단 기본적으로


1. sdk Manager로 sdk 재설치, tool 복구

2. jdk 버전 다운그레이드 (1.7까지)

3. Keystore 새로 생성 혹은 삭제

4. targetAPILevel, minAPILevel 설정


정말 당연히 sdk나 안드로이드 api 버전을 잘못 타게팅해서 생긴 오류는 아니었으니, 위는 전혀 도움이 안 되었습니다.


그러다 스택오버플로에서 한참을 헤엄치다 발견한 한 줄기 빛.......



Unalign된 apk파일이 2GB가 넘어가면 ziparchive툴이 동작하지 않는다는 이슈가 있답니다. 엥? 설마?

하지만 sdk 버그 릴리즈 항목을 찾아봐도 이미 1.7 업데이트 이전에 수정이 된 항목이기도 하던데.. 하면서 지푸라기 잡는 심정으로 부랴부랴

리소스 크기를 줄여서 다시 빌드를 해봅니다. 딱 2GB 아래로만 떨굴 심산으로.





ㅋㅋ..ㅋㅋㅋ.. 성공.. 했네요 apk도 정상적으로 뽑히고, 빌드도 됩니다.


아마 최근에 대량의 리소스들이 추가되면서 Asset용량이 비정상적으로 불어난 것 같은데


어차피 이 리소스들, 동적할당이 아니라 전부 패키지에 포함이 되는 구조에요. 사실 앱이 2GB 이상 넘어가는것 자체가 말이 안 되는 일이기도 합니다.


요 다음 몇주간은 리소스 리팩토링으로 지새겠군요. 일단 빌드는 되니까 다행이기도 하고....... ㅠㅠㅠ흑흑흑..



정확히 1년만의 포스팅 (...) 왜냐하면 실컷 놀았거든요.


-------------------------------------------------------------



지금 개발하고 있는 프로젝트에선.. 정말 특이하게 단순정보만 담은 리소스들도 DB로 관리하고 있습니다.


아니, DB로만 관리하는게 아니라 어디엔 xml, 어디엔 스크립트 안에서(;;) 아주 중구난방이긴 한데

기획자들에게 직접 DB수정을 요할 수는 없으니 대부분 요청사항을 전달받을 때 Excel로 작업하곤 합니다.


보통 데이터테이블을 csv형식으로 뽑아주면, 이를 엑셀로 Import시켜 기획팀에서 수정 후 다시 엑셀을 제가 받아오는.. 그런 식인데

어찌됐든 요놈을 db에 집어넣고, 파싱하고, bytes파일로 바꾸고 하는 일은 온전히 제 일이란 말이죠. 그런데 말썽이.. 좀 있었습니다.




뭐 예를 들어, column별로 정리된 데이터테이블이 있다고 칩시다.



제 인생은 되는 일이 잘 없더라구요.


일단 구글링을 좀 해보니, DB INSERT시 칼럼이 NOT NULL 여부에 관계없이 실수, 정수형 데이타에 빈 문자열을 입력하면 발생하는 오류랍니다.

위에 엑셀 파일에 null값을 넣었는데, 처음엔 저게 빈칸으로 왔었어요.


일일히 쿼리문 써가면서 수정하자니 머리가 나빠 손이 고생하는 느낌도 들고..


뭐 여튼 해결방법은, 제한설정을 좀 풀어주면 됩니다. 자료형이 비좁다면 더 크게 바꾸고, 65535바이트 이상이라면 MEDIUMTEXT로 바꾸고 뭐 그런 식이요.


그리고 null값 관련은 ini 설정에서 


sql-mode=”STRICT_TRANS_TABLES,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION”


추가해주시면 됩니다.


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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
[System.Serializable]
public class SqliteItem
{
    public string Name;
    public string Path;
    public string Data;
    public string[] querys;
    public SqliteItem(string name, string path, string data)
    {
        Name = name;
        Path = path;
        Data = data;
    }
}
public class Converter : MonoBehaviour
{
    public SqliteItem[] Items;
    public void Btn_Convert()
    {
        StartCoroutine(ReadFile());
    }
    public void Btn_Exit()
    {
        Application.Quit();
    }
    //불러온 .sql 파일들을 sqlite 데이터베이스 파일로 변환하여 생성하기
    void Write()
    {
        foreach (var item in Items)
        {
            SqliteDB db = new SqliteDB(item.Name);
            db.connection();
            for (int i = 0; i < item.querys.Length; i++)
            {
                db.ApplyQuery(item.querys[i]);
            }
            db.Disconnection();
        }
    }
 
 
    //.sql 파일들을 불러와 저장하기
    IEnumerator ReadFile()
    {
        DirectoryInfo di = new DirectoryInfo(Application.streamingAssetsPath);
 
        List<SqliteItem> tmp = new List<SqliteItem>();
        foreach (var item in di.GetFiles("*.sql"))
        {
            WWW www = new WWW(Application.streamingAssetsPath + "/" + item.Name);
            while (!www.isDone)
            {
                Debug.Log("대기중");
                yield return new WaitForFixedUpdate();
            }
            tmp.Add(new SqliteItem(item.Name.Split('.')[0], item.DirectoryName, www.text));
        }
        if (tmp.Count == 0)
        {
            Debug.Log("파일이 없습니다");
        }
        Items = tmp.ToArray();
        StartCoroutine(Convert());
    }
    IEnumerator Convert()
    {
        foreach (var item in Items)
        {
            StringReader sr = new StringReader(item.Data);
            List<string> querys = new List<string>();
            string line = sr.ReadLine();
            string query = "";
            if (line.Contains("    "))
            {
                line = line.Replace("    "" ");
            }
            while (line != null)
            {
                if (line.Contains("--"|| line.Contains("/*"|| line.Contains("*/"|| line.Contains("USE"||
                    line.Contains("COMMENT"|| line.Contains("UNIQUE KEY"||
                    line.Contains("ENGINE"|| line.Contains("PRIMARY KEY"|| line.Contains("KEY `"))
                {
                    if(line.Contains("DATABASE"))
                    {
                        line = sr.ReadLine();
                        continue;
                    }
 
                    if(line.Contains("CREATE")||line.Contains("INSERT INTO"))
                    {
                        query += line;
                        line = sr.ReadLine();
                        continue;
                    }
                    line = sr.ReadLine();
                    continue;
                }
                if (line.Equals(""))
                {
                    if (!query.Equals(""))
                    {
                        query = SqlToSqlite(query);
                        querys.Add(query);
                        query = "";
                    }
                }
                else
                {
                    if(line.Contains("INSERT IGNORE INTO"))
                    {
                        line = line.Replace("INSERT IGNORE INTO""INSERT OR IGNORE INTO");
                    }
 
                    query += line;
                }
                line = sr.ReadLine();
            }
            //M용 유저 테이블 추가
            querys.Add();
 
            item.querys = querys.ToArray();
            yield return null;
        }
        Write();
    }
    string SqlToSqlite(string query)
    {
        string[] tmp = query.Split(' ');
        query = "";
        for (int i = 0; i < tmp.Length; i++)
        {
            if (tmp[i].Contains("int("))
                tmp[i] = "INTEGER";
 
            if (tmp[i].Contains("varchar("))
            {
                tmp[i] = "TEXT";
            }
 
            if (tmp[i].Contains("AUTO_INCREMENT"))
                tmp[i] = tmp[i].Replace("AUTO_INCREMENT""PRIMARY KEY AUTOINCREMENT");
 
            if (tmp[i].Contains("float"))
                tmp[i] = "NUMERIC";
 
 
 
            query += tmp[i] + " ";
        }
        query = query.TrimEnd();
        query += ")";
        query = query.Replace("))"")");
        query = query.Replace(";)"";");
        query = query.Replace(",)"")");
        query = query.Replace("  "" ");
        return query;
    }
}
 
cs



그리고 아주 약간의 하드코딩을 통해, sql을 그대로 .db로 바꿔주는 함수를 짜둡니다.

그렇게 db파일을 퉤 하고 뱉어내면 확장자를 .bytes로 바꾸어 주면 됩니다.


그래도 일단 이렇게 만들어두면 XML, DB, bytes, csv 형식을 자유로이 왔다갔다 할 수 있어서 앞으론 좀 편하겠네요.



언제부턴가 마비노기 염색 도우미가 안 되기 시작했다. 자꾸만 막대 색상변경이 감지되었다고.


읽어들이는 막대색상이 자꾸만 변하길래 아 이건, 읽어들이는 픽셀값이 막대색이 아닌 배경색임을 짐작,


뭔가 읽어들이는 픽셀값 위치가 어긋나있음을 예상했다.





 -> 



이유인 즉슨 염색바 진행픽셀 길이(!!)가 더 길어졌다는 점.

아니, 창 크기 자체가 달라졌으니 자꾸 애꿎은 배경색만 스포이드로 인식한다는게 이유인 듯 했다.


그럼 어떡해 픽셀만 옮겨줘야지 - "-


일단 프로그램부터 뜯어보기로 한다.





데...델파이?! -_-


끽해야 닷넷이겠거니 하고 아무 디컴파일러를 쓸 생각이었는데 델파이라니. orz

델파이 언어는 저번 OpenCV 호작질하며 컴파일하며 써본 적이 있다. 마침 디컴파일러는 DeDe가 있어서 써보기로 함.


물론 DeDe 단독 말고 디버깅 도구와 함께 쓰도록 한다.




'염색 진행 막대 색상 . . .'으로 시작하는 String Reference를 찾는다. 여기렸다.







마찬가지로 관련 클래스의 Seek 픽셀들을 전부 오른쪽으로 더 당김

전부 수정했으면 다시 컴파일하고 나머지는 백업해둠





Hex값이 잘 바뀌어있는지 확인하고



실행




콘솔 잘 올라오고

(Modified By Nalchi -"-)





된다!!!




(0927 추가 : 마도카에 업데이트 된 버전이 올라와있었다고 한다. 그것도 이 주 전에. -_-)





회사 동기중 한명이 ifstream의 getline이 fgets보다 성능이 좋다는 썰을 풀길래 이건 무슨 신박한 개소리인가 싶어 간단히 속도 테스트를 해보았습니다. 테스트 환경은 윈10 64bit, 약 14M 정도의 로그 데이터파일을 기준으로 짜둠!


..일단 결론부터 말하면, ifstream은 쓸 것이 못 됩니다. 수백메가도 아닌 단 몇천줄 로그파일 읽는데에도 시간이 훨씬 차이가 납니다.





1) getline()으로 직접 읽기. 버퍼를 지정하고 직접 넘겨주기에 ifstream 중에서 그나마 성증이 좋음


void test_ifstream1() {
	std::ifstream inFile;
	inFile.open(TEST_FILE);

	if (!inFile.is_open()) exit(1);

	char buf[512];
	while (!inFile.getline(buf, sizeof(buf)).eof()) {

	}
}



2) istreambuf_iterator 사용. 모든 내용은 vector<char>형태로 저장. 나름 버퍼를 사용하는구나 싶어 성능이 좋겠다 생각했지만, 정반대이다.


void test_ifstream2() { std::ifstream inFile; inFile.open(TEST_FILE); if (!inFile.is_open()) exit(1); std::istreambuf_iterator<char> begin(inFile); std::istreambuf_iterator<char> end; std::vector<char> contents(begin, end); }



3) fopen()과 fread()를 사용. 한꺼번에 512byte 정도를 읽어오며, 딱 한 줄씩 읽어오지는 않는다. ifstream을 사용할때보다 약 7배이상 성능이 좋음.

   대신, 한 줄씩 읽어서 처리할 경우 문자별 매칭을 수행해야하는 번거로움이 생긴다.


void test_osfile1() {
    FILE *fp = fopen(TEST_FILE, "r");

    if (NULL == fp) exit(1);

    char buf[512];

    size_t len = 0;
    while (!feof(fp)) {
        len = fread(buf, sizeof(buf[0]), sizeof(buf)/sizeof(buf[0]), fp);
    }
    fclose(fp);
}



4) fgets() 함수. 위의 fread보단 시간이 약-간 더 걸렸다.


void FREAD_TEST() {
	FILE *fp = fopen(TEST_FILE, "rb");
	if (NULL == fp)
		exit(1);

	char buf[BUF_SIZE] = { 0, };

	char buf[BUF_SIZE] = { 0, };
	while (NULL != fgets(buf, sizeof(buf), fp)) {}

	fclose(fp);
}




normal read에선 크게는 7000msec 까지 차이가 나더이다. 이는 엄청난 수치죠. binary read라면 워낙 빠르기에 차이가 미미할 수도 있지만..


사실 여러 운영체제에 포팅하는 경우 이식성을 높이기 위해선 ifstream을 쓰는 것이 가장 좋을수도 있습니다. 하지만 성능이 문제라면.. 다른 공통으로 사용할 수 있는 라이브러리는 찾는게 방안일 수도 있겠네요. 안정성과 검증을 해야하는 문제도 있고..


하지만 fopen()은 공통 c라이브러리이기 때문에 대부분 호환 가능해서 이식성에 어려움은 없습니다. 문제는 기존 C++의 stl과 연동이 자연스럽지 않기에 이를 어떻게 풀어야 하는가. 즉, 해야할 일들이 많이 생기게는 되겠네요 = _=


어쨌든! 단순 성능비교를 할땐 fread()의 성능이 압도적인걸로!






보통 자바에서는 자료를 읽거나 쓰기 위해 Stream을 사용하고, 자바는 두 가지 형태의 Stream을 지원합니다.


Reader/Writer, Input/OutputStream이 그것인데, Reader/Writer는 High-Level의 I/O(String이라던가..)을 지원하고, Input/OutputStream은 Low-Level의 I/O(byte)를 지원합니다. 속도 향상을 위해서는 BufferedStream을 사용하지만 BufferedStream의 경우 버퍼의 기본값은 2k입니다. 이 값은 조정할 수 있으나 자료의 용량이 클 경우 메모리가 많이 필요하기 때문에 BufferedStream을 사용할 경우 여러가지 사항을 고려해야 합니다.



아래는 BufferedStream을 사용하지 않고 I/O를 했을 경우의 예제입니다. 370k의 JPEG 파일을 복사하는데 10800ms.


public static void copy(String from, String to) throws IOException { InputStream in = null; OutputStream out = null; try { in = new FileInputStream(from); out = new FileOutputStream(to); while (true) { int data = in.read(); if (data == -1) break; out.write(data); } in.close(); out.close(); } finally { if (in != null) in.close(); if (out != null) out.close(); } }






BufferedStream을 사용해서 퍼포먼스를 개선한 예제입니다. 같은 파일을 복사하는 데에 130ms밖에 걸리지 않습니다.


public static void copy(String from, String to) throws IOException { InputStream in = null; OutputStream out = null; try { in = new BufferedInputStream(new FileInputStream(from)); out = new BufferedOutputStream(new FileOutputStream(to)); while (true) { int data = in.read(); if (data == -1) break; out.write(data); } } finally { if (in != null) in.close(); if (out != null) out.close(); } }






while 루프를 사용하지 않고 배열을 사용함으로써 퍼포먼스를 개선한 예제. 같은 파일을 복사하는 데에 33ms.


public static void copy(String from, String to) throws IOException { InputStream in = null; OutputStream out = null; try { in = new FileInputStream(from); out = new FileOutputStream(to); int length = in.available(); byte[] bytes = new byte[length]; in.read(bytes); out.write(bytes); } finally { if (in != null) in.close(); if (out != null) out.close(); } }





하지만 바로 위 코드는 byte 배열로 선언되는 메모리 버퍼의 크기가 실제 파일의 크기와 동일해야 합니다. 이에 따라 두 가지 문제점이 발생할 수 있는데요, 첫 번째는 파일의 용량이 클 경우 상당한 메모리 낭비를 초래한다는 점. 두 번째는 Copy() 메소드가 수행될 대마다 new byte[]에 의해 버퍼가 새로 만들어진다는 점. 마일 파일의 용량이 클 경우 버퍼가 만들어지고 Garbage Collector에 의해 수집될 때 상당한 오버헤드를 초래할 수 있습니다.


예제4) Improved Custom Buffered Copy
static final int BUFF_SIZE=100000;
static final byte[] buffer=new byte[BUFF_SIZE];
 
public static void copy(String from, String to) throws IOException {
    InputStream in=null;
    OutputStream out=null;
 
    try {
        in=new FileInputStream(from);
        out=new FileOutputStream(to);
     
        while(true) {
            synchronized (buffer) {
                int amountRead = in.read(buffer);
                 
                if (amountRead == -1)
                    break;
     
                out.write(buffer, 0, amountRead);
            }
        }
    } finally {
        if (in!=null)
            in.close();
         
        if (out!=null)
            out.close();
    }
}



크기가 100k인 byte배열을 임시버퍼로 지정하고 이를 static으로 선언함으로써 퍼포먼스를 개선했습니다. 이 코드로 같은 파일을 복사하는데 22ms밖에 걸리지 않아요. static buffer의 사용으로 I/O작업을 수행할 경우 발생하는 문제점을 해결하기 위해 synchronized block을 사용했습니다. 비록 sync를 사용함에 따라 성능 저하를 일으킬 수도 있지만, 실제 while 루프에 머무는 시간이 극히 짧기 때문에 퍼포먼스에 문제는 없습니다. 테스트에 의하면 synchronized 버전과 unsynchronized 버전 모두 같은 시간에 수행을 완료했었습니다.





NFC 태그를 단말에서 인식하게 되면 그 정보는 인텐트의 부가 데이터로 넣어져 브로드캐스팅 됩니다. 이때, 인텐트에 들어있는 NFC 태그의 값을 확인하려면 'EXTRA_TAG'를 키값으로 하여 Parcelable 객체를 참조하면 됩니다. 브로그캐스팅 되는 인텐트를 받기 위한 대상 액티비를 결정하는 과정 중에 가장 먼저 확인하는 것이 되려면.. 다음과 같은 enableForegroundDispatch() 메소드를 호출하면 됩니다.






[Reference]


public void enableForegroundDispatch (Activity activity, PendingIntent intent, InterFilter[] filters, String[][] techLists)


public void disableForegroundDispatch (Activity activity)









이 메소드들은 NfcAdapter 객체에 정의되어 있습니다. 다음과 같이 NfdAdapter 객체를 참조한 후 액티비티의 onResume() 메소드 안에서는 enableForegroundDispatch()를, 그리고 onPause() 메소드 안에서는 disableForegroundDispatch()를 실행합니다.








[Reference]


public static NfcAdapter getDefaultAdapter (Context context)







만약 이렇게 참조한 NfcAdapter 객체가 활성화되어 있지 않다면 NFC를 사용할 수 없는 상태가 되므로 널값인지 여부와 함께 isEnabled()메소드를 이용해 활성화 여부를 확인할 수 있습니다.


enableForegroundDispatch() 메소드의 파라미터로 전달되는 PendingIntent 객체는 NFC 태그가 인식되어 브로드캐스팅으로 인텐트를 받았을 대, 실행할 액티비티를 지정하는 역할을 합니다. 세 번째와 네 번째 파라미터는 필터와 사용할 상세 기술의 내용이 들어갑니다.




 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
using System;  

public struct MyStruct  
{  
    public int Width;  
}  

public class ListView  
{  
    MyStruct ms;  
    public MyStruct Size  
    {  
        get { return ms; }  
        set { ms = value; }  
    }  
}  

public class MyClass  
{  
    public MyClass()  
    {  
        ListView lvi;  
        lvi = new ListView();  
        lvi.Size.Width = 5; // CS1612                                                                                  
    }  

    public static void Main()   
    {  
        MyClass mc = new MyClass();  
        // Keep the console open in debug mode.  
        Console.WriteLine("Press any key to exit.");  
        Console.ReadKey();     
    }  
}  


C++에서 C#으로 옮기면서 겪는 백만스물두 가지 정도 고통 중의 하나입니다.


보통 List 안에 구조체 변수를 넣고 돌리는 경우가 종종 있는데, 무려 C#에서는 컴파일 에러가 나는 코드입니다.



이유가 뭐인고 하니 인덱스에서 값 형식을 찾아갈때는 그 개체 자체의 참조변수가 아닌 '복사본'을 얻게 된다는 것.


이 '복사본'은 저장소 위치를 가르키는 것이 아닌 단순한 메소드이기 때문에 인덱스에 저장되진 않는다는 겁니다.


한 마디로 개체참조를 하려고 했더니 헛손가락질을 한다는 것.



'당연한 거 아니야?' 라고 한다면 그런거겠지..만 이게 익숙해진 사람들에겐 의외로 짜증나는 것이거든요 -_-;

생각보다 해결 방법은 많습니다.


1. 구조체 선언 시 접근속성 부여


2. 구조체에 고유한 인스턴스를 만들고, 해당 필드를 수정 후 다음 전체 구조체를 속성에 다시 할당하기 (2중 구조체)


3. 구조체를 클래스로 변경하기 (?)




 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public MyClass()  
    {  
        //ListView lvi;  
        //lvi = new ListView();  
        //lvi.Size.Width = 5; // CS1612                                                                                

        MyStruct ms;  
        ms.Width = 5;  
        lvi.Size = ms;
    }  


그냥 재할당을 해주면 단순히 해결되긴 합니다 ~_~


블로그를 쓰다 보면 첫 페이지에 특정한 무언가를 리다이렉트 시키고 싶을 때가 많습니다.


예를 들어 공지사항이라던가, 방문객들에게 제일 처음 보여주고픈 것들이라던가, 혹은 단지 대문의 역할로 말이죠.





사실 이전 네이버 블로그에서도 아주 '비슷한' 기능을 제공하긴 했었습니다.


특정 카테고리를 기본으로 보여주는 기능을 설정할 수 있었는데, 저같은 경우는 '프롤로그' 카테고리를 따로 만들고 단 하나의 포스팅만 한 뒤


프롤로그 카테고리를 기본으로 설정하는 방법으로 나름의 대문(?)을 꾸몄었습니다.





요렇게 카테고리 기본 설정으로 해놓으면





첫 페이지가 설정해둔 카테고리의 포스트 내용으로 설정된다.






하지만 티스토리는 아무리 찾아봐도 없어요. 아니, 제가 자세히 알아보지 않았을 수도 있습니다.


저는 아주 화려하고 볼거리 많은 카탈로그 모음집 같은 프롤로그 홈 화면을 원하지 않아요. 간결한 것이 가장 좋습니다.


내가 원하는 페이지 하나 띄우는게 그렇게 어렵단 말이더냐 -_-a 하며 기능들을 찾아보다가...




아니 맙소샄ㅋㅋㅋㅋ 맞다 티스토리의 스킨 페이지들은 전체적으로 html (심지어 css까지) 수정이 가능했었습니다.


다행히 제가 쓰고 있는 스킨은 자유 수정이 가능한 라이센스네요. 문득 떠오르는 것이 있으니 한 가지만 수정을 봅시다.




head 끝부분에 간단하게 스크립트 하나만 추가해주시면 됩니다.


window.location.pathname는 현재 호스트 페이지의 주소값을 string명세로 반환합니다.


예를 들어서 http://example.org/myfolder/mypage.html 이라는 페이지에선 /myfolder/mypage.html을 반환하겠네요.


보통 티스토리 홈 화면은 제가 쓰는 주소 그대로이니까! 리턴값은 /로만 잡고 리다이렉트 할 주소를 href 코드에 적어주시면 됩니다.


무사히 코드를 갈아끼우고 저장을 누르셨다면..



짜잔 ~_~ 첫 화면이 자동으로 제가 등록해놓은 포스팅 주소로 이동하게 됩니다!




---- 추가


모바일로 접속시엔 '../m'으로 리다이렉팅 됩니다. 고로 pathname 수정!


+ Recent posts