Из этой главы, да и из всей этой книги понятно, что самые лакомые куски программирования под iOS включены в публичные фреймворки, но не в SDK. Неофициальная политика Apple насчет этого проста: вы можете всё это использовать, но только на свой страх и риск. Ваш код может сломаться при следующем обновлении прошивки. Вам самим придётся искать компромисс между риском и прибылью.
Erica Sadun, The iPhone Developer's CookBook
Дисклеймеры
- Приведенные здесь куски кода работают на обычных iPhone (включая 4S) и iPad (включая new iPad) и не требуют jailbreak.
- Все решения написаны и протестированы на iOS 5. Все решения также протестированы на совместимость с iOS 4.3, т.е. работают с iOS 4, если не сказано обратное. Основная часть статьи была написана до выхода iOS 6, так что приведенные решения не тестировались на совместимость с iOS 6.
- Использование недокументированных API может привести к тому, что ваше приложение не допустят в AppStore. А может и не привести :-)
Для тех, кому интересно, как Apple опрделяет использование приваетных API:«Как Apple узнаёт, что ты используешь приватные API?» - Apple может изменить реализацию вместе со следующим релизом iOS, и в вашем коде что-то сломается. Впрочем, это решаемо, и ничем принципиально не отличается от реализации обратной совместимости для документированных API. Ниже я рассмотрел эту проблему чуть подробнее.
- Я не могу гарантировать, что у найденных мной API нет побочных эффектов. Используйте на свой страх и риск.
- Лицензионное соглашение Apple Developer Program запрещает реверс-инжинеринг iOS.
- Статья в процессе доработки. Конструктивная критика приветствуется!
Краткая инструкция по поиску в SDK
Допустим, вам нужно сделать что-то, выходящее за рамки официальной документации. Например, изменить уровень подсветки экрана (до iOS 5 этого не было в документированной части SDK). Известно, что программисты Apple как правило дают функциям и переменным осмысленные и выразительные названия, этим мы и воспользуемся для поиска в SDK. Для этого выберем несколько слов, относящихся к теме, например, brightness, level, screen. Запустим скрипт LookSDKForSymbol.sh (это моя обертка над nm
; об этом скрипте и других используемых инструментах написано далее, в разделе «Инструменты») с ключевыми словами в качестве параметров. Скрипт выдаёт найденные в объектном файле символы (т.е. названия классов, функций, переменных). Пример выдачи:
$ LookSDKForSymbol.sh light level
U _UIBacklightLevelChangedNotification
Found in ./System/Library/CoreServices/SpringBoard.app/SpringBoard001b43c4 t -[UIApplication backlightLevel]
001b4360 t -[UIApplication setBacklightLevel:]
0025ce54 t -[UIDevice _backlightLevel]
0025ce40 t -[UIDevice _setBacklightLevel:]
… и ещё несколько десятков символов
Большую часть результатов можно сразу отбросить, например -[UIApplication backlightLevel]
возвращает значение подсветки, а не устанавливает его.
Оставшиеся, если их не более нескольких десятков, можно попытаться скормить гуглу. Случается, что кто-то уже занимался исследованием API, связанных с найденными символами, и в этом случае задача, считай, решена. В более сложных случаях приходится заниматься реверс-инжинерингом, то есть выяснять, как работают найденные функции, как использовать найденных оповещения и тому подобное.
Символьные строки, выдаваемые утилитой, делятся на следующие категории:
- Objective-C и С++ функции, классы, структуры и так далее. Всё что относится к Objective-C содержит квадратные скобки([]) либо знаки доллара ($). C++ функции как правило содержатся в каком-нибудь namespace'е, и поэтому в их названии содержиться символ разрешения пространства имён, два двоеточия (::).
- Objective-C блоки. Они имеют следующий общий вид:
___{число}{вызывающая данный блок функция}_block_invoke_{число2}
Например:
___22-[AXSystemServer init]_block_invoke_0
- Pure C функции.
- Objective-C оповещения. Заканчиваются на Notification, например
_SBMenuButtonPressedNotification
. - Ключи/константы. Обычно начинаются на k, например: _
kCFUserNotificationAlternateButtonTitleKey
.
Дальнейшие действия зависят от категории символа.
- Генерируем заголовочный файл для данного фреймворка:
class-dump-z Foundation > $/iOS_private_headers/Foundation.h
В большинстве случаев, сгенерированного заголовчного файла достаточно: в нем должны быть довольно хорошо описаны иерархии наследования классов, структуры, методы и т.д, чтобы потратив немного времени можно разобраться с API и использовать его в своём предложении.
К сожалению, иногда информации содержащейся в заголовочном файле недостаточно, чтобы заставить код работать, и тогда приходится анализировать ассемблерный код, сгенерированный otool.
Hint по дизассемблированию Objective-C кода: почти наверняка вы столкнетесь с вызовами функций типа objc_msgSend (отправка сообщения объекту). В качестве первого параметра всегда идет указатель на объект, а вторым — указатель на селектор (selector), т.е. указатель на строку, являющуюся названием метода (остальные «обычные» аргументы идут третьим, четвертым и т.д. аргументами). Определить, что за сообщение отправляется в данном случае, поможет
hexdump
. - Про это можно сразу забыть. Блоки (обычно) локальны, их нельзя вызвать из своего кода.
- Самый сложный вариант. В самых простых случаях можно подобрать сигнатуру для функции, в остальных — только дизассемблирование. Больше об этом можно узнать в разделе «Как узнать сигнатуру неизвестной функции?».
- Начнем с того, что попытаемся отловить оповещения в одном из трёх основных центров оповещений (это Local, Darwin и CoreTelephony). Если оповещения такого типа не приходят, дело может быть в одной из двух вещей:
— Оповещения такого типа приходитя в отдельный, специальный центр оповещений. Cледует поискать следы такого центра оповещений в том же фреймворке, к каторому принадлежит найденное оповещение.
— Доставка оповещений отключена. Попытаться найти механизм включения доставки оповещений такого типа. - В этом случае, скорее всего существует либо функция, которая принимает данную константу в виде параметра, либо словарь, в котором данная константа является ключом. В любом случае, следует искать функцию или метод, название которых начинаются с того же слова (например: константа
kLockdownDeviceColorKey
-> функцияlockdown_copy_value(...)
;
Как узнать сигнатуру неизвестной функции?
1. Найти в интернете, как это не банально. Мне довольно часто попадались китайский сайты, были корейский и японский сайты с очень полезной информацией. Обычно самого кода уже достаточно, чтобы понять что происходит и как используется данная функция, данный класс и т.д. Спасибо многословности и выразительности Objective-C!
2. Для многих простых функций, можно попытаться угадать сигнатуру. Внимание, это может быть довольно опасно.
Использование некоторые простые функции, таких как GSEventSetBackLightLevel, самоочевидно.
void GSEventSetBackLightLevel(float level);
Для многих других я использовал следующий трюк (на примере функции SBGetRingerSwitchState):
SInt32 ret = 5, out1 = 1, out2 = 2, out3 = 3, out4 = 4;
void *libHandle = dlopen(SPRINGBOARD_SERVICES_PATH, RTLD_LAZY);
SInt32 (*SBGetRingerSwitchState)(SInt32*,SInt32*,SInt32*,SInt32*) = dlsym(libHandle, "SBGetRingerSwitchState");
ret = SBGetRingerSwitchState(&out1, &out2, &out3, &out4);
NSLog(@"%x %x %x %x %x", ret, out1, out2, out3, out4);
В результате работы этого кода выяснилось, что
1) функция возвращала значение 0x10000003
, не зависящее от реального положения переключателя.
2) Переменная out2
изменила свое значение на self. Возвращаемое значение также не зависит от переключателя.
3) Остальные переменные не изменили свое значение.
Из 1) я сделал вывод функция возвращает значене типа kern_return_t
, так как 0x10000003
соответствует системной ошибке MACH_SEND_INVALID_DEST
. По видимому, ошибка указывала на неправильный порт [в данном случае порт — это абстракци ядра mach (mach kernel), характеризующая права и приоритет процесса]. Как правило, если в вызове функции используется номер порта, то он идет первым аргументом. Из 2) следует, что через второй аргумент функция возвращает некое значение по ссылке.
В результате этих нехитрых действий получается следующая сигнатура:
kern_return_t SBGetRingerSwitchState(mach_port_t port, SInt32 *state);
Кстати, если в названии функции присутствует слово get, то согласно naming conventions Objective-C эта функция должна возвращать значение по ссылке. Это также видно из приведенного примера.
3. Дизассемблирование. На примере все той же SBGetRingerSwitchState. Используем otool:
$ otool -p _SBGetRingerSwitchState -tV -arch armv6 SpringBoardServices | less
000038cc b5f0 push {r4, r5, r6, r7, lr}
000038ce af03 add r7, sp, #12
000038d0 b092 sub sp, #72
000038d2 aa06 add r2, sp, #24 // значение регистра r2 затирается
000038d4 9205 str r2, [sp, #20]
000038d6 ac08 add r4, sp, #32 // … как и регистра r4
000038d8 ab0f add r3, sp, #60 // … и r3
000038da 9304 str r3, [sp, #16]
000038dc 9103 str r1, [sp, #12] // значение r1 сохраняется в стеке
000038de 4925 ldr r1, [pc, #148] (0x3974)
000038e0 6011 str r1, [r2, #0]
000038e2 6020 str r0, [r4, #0] // значение r0 также сохраняется в стеке
…
Из этого кода, используя даже поверхностные знания arm-ассемблера, можно предположить, что функция принимает два аргумента типа «слово» (word)
Выходит, что у функции два аргумента. Идем дальше, в самый конец.
…
00003964 9e04 ldr r6, [sp, #16]
00003966 6836 ldr r6, [r6, #0]
00003968 9903 ldr r1, [sp, #12]
0000396a 600e str r6, [r1, #0]
// примерно соответствует (в терминах языка си): *r1 = r6; т.е. по адресу, хранящемуся в r1 записывается значение из r6;
// Это значит, что функция возвращает значение по ссылке
0000396c 462e mov r6, r5
0000396e 4630 mov r0, r6
// результат выполнения функции помещается в r0
00003970 b012 add sp, #72
00003972 bdf0 pop {r4, r5, r6, r7, pc}
…
В сухом остатке получаем:
int SBGetRingerSwitchState(int arg1, int* arg2);
Продолжая анализировать этот асcемблерный код, уточняем типы и приходим к окончательному варианту:
kern_return_t SBGetRingerSwitchState(mach_port_t port, SInt32 *state);
Разные прошивки и разные устройства: что может сломаться и как это исправить?
Понятно, что недокументированные API вовсе не обязательно работают на всех устройствах одинаково. По моему опыту, чаще всего ничего не меняется, и API работает одинаково на всех устройствах и всех прошивках. Так, например, все функции расширения UIDevice-IOKitExtensions (кроме определения IMEI) работают одинаково хорошо на всех устройствах и всех прошивках. Какие изменения могут произойти при обновлении iOS?
Вот несколько практических вариантов.
- Может появиться официально документированный программный интерфейс, при этом недокументированный интерфейс, как правило, продолжает работать. Пример:
void GSEventSetBacklightLevel(float level); // работает во всех версиях iOS -[UIDevice setBrightness: (CGFloat)brightness]; // появилось в iOS 5.0
- Программные интерфейсы переносятся в другой фреймворк. Apple может объединить несколько фреймворков в один, переименовать или удалить фреймворк. Например, функции для работы с WiFi (Apple80211Open, Apple80211Close и так далее) были перенесены из Aeropuerto.dylib в IPConfiguration.dylib.
- API могут просто удалить, особенно если она связана с уязвимостью.
Для того чтобы избежать проблем совместимости, соблюдайте простые правила: проверяйте наличие функций (например, с помощью -[NSObject respondsToSelector:]
), классов (NSClassFromString(@"SomeClass")
вернет nil
в случае отсутствия класса SomeClass
) и т.д., а также заранее подумайте, что должна делать программа в случае, если API отсутствует. При использовании динамической линковки библотек следует также всегда проверять возвращаемые значения dlsym(...) и dlopen(...) на равенство NULL.
Примеры
Пример 1:
Определение положения бокового переключателя вибро (a.k.a. Ring/Silent switch, Mute switch)
Одной из задач, которые стояли передо мной, было определение положения бокового переключателя, который в оригинале называется ring/silent switch. Этот переключатель используется для переключения между «тихим» и обычном/«громким» режимами в айфоне и айпаде. Поиск по StackOverflow дал решение:
#import <AudioToolbox/AudioToolbox.h>
...
/*
Возвращаемое значение:
0: тихий режим
1: обычный режим
*/
int switchState()
{
// ...
// Инициализируем и активируем аудиосессию, устанавливаем
// категорию в значение kAudioSessionCategoryAmbient
// ...
UInt32 size = sizeof(CFStringRef));
CFStringRef route;
AudioSessionGetProperty(kAudioSessionProperty_AudioRoute, &size, &route);
// Получаем описание текущего аудиовыхода
CFIndex len = CFStringGetLength(route);
return (len > 0); // Если результат - пустая строка, значит телефон находится в "тихом" режиме
}
Которое, впрочем, не работает в iOS 5. Не сработало и использование более нового API (kAudioSessionProperty_AudioRouteDescription) которое дает расширенную информацию об аудиовходах и -выходах. (AUDIOROUTE)
Мои дальнейшие поиски по StackOverflow вывели меня на этот пост. В нем описывается библиотечная функция AudioServicesAddSystemSoundCompletion(), чьё нестандартное поведение рассматривалось разработчиками как баг.
#import <AudioToolbox/AudioToolbox.h>
...
void playSound()
{
AudioServicesAddSystemSoundCompletion(MySoundId, NULL, NULL, MyAudioServicesSystemSoundCompletionProc,
AudioServicesPlaySystemSound(MySoundId);
}
void MyAudioServicesSystemSoundCompletionProc (SystemSoundID ssID, void *clientData)
{
// Проигрывание звука завершено
NSLog(@"Playback has been finished");
}
Нестандартное поведение заключается в том, что вызов колбэка MyAudioServicesSystemSoundCompletionProc состоится в конце проигрывания звука в обычном режиме, но сразу после вызова AudioServicesPlaySystemSound в «тихом» режиме. Это создает лазейку для определения текущего состояния переключателя. Если, например, длина аудиофайла что мы проигрываем равна 1 с, то разница во времени вызова MyAudioServicesSystemSoundCompletionProc() в «тихом» и громком режиме составляет 1 c. На этом я построил свое второе, асинхронное решение для определения положения бокового переключателя. Вот оно:
#import <AudioToolbox/AudioToolbox.h>
#import "MuteSwitchTet.h"
...
enum MuteSwitchStates {
kMuteSwitchUndefined = -1,
kSoundless = 0,
kSound = 1
};
@implementation MuteSwitchTest
...
void MyAudioServicesSystemSoundCompletionProc (SystemSoundID ssID, void *clientData)
{
// "тихий" режим
MuteSwitchTest *self = (MuteSwitchTest*)clientData;
[NSObject cancelPreviousPerformRequestsWithTarget:self];
self.muteSwitchState = kSoundless;
}
- (void) cancelSystemSoundCompletion
{
// "громкий" режим
AudioServicesRemoveSystemSoundCompletion(SoundID);
self.muteSwitchState = kSound;
}
- (void) startPlayback
{
AudioServicesAddSystemSoundCompletion(SoundID, NULL, NULL, MyAudioServicesSystemSoundCompletionProc, self);
AudioServicesPlaySystemSound(SoundID);
[self performSelector:@selector(cancelSystemSoundCompletion) withObject:nil afterDelay:0.1];
}
...
@end
Хотя это новое решение и было рабочим, оно не устраивало меня по нескольким причинам. Во-первых, оно было асинхронным и работало с ощутимой задержкой (около 1/10 секунды). Снижение задержки вело к ложным срабатываниям. Во-вторых, был побочный эффект — сам проигрываемый звук, который звучал достаточно громко чтобы смутить пользователя. Позже я искусственно выкрутил громкость в ноль в аудиоредакторе. В-третьих, это был уже слишком похоже на грязный хак, хотя это, например, не помешало создателям VSSilentSwitch продавать свое решение, по всей видимости основанное на том же эффекте.
Примерно через месяц я вернулся к этой проблеме. Я начал использовать команду nm для поиска символов в объектных файлах, на её основе я написал простейший shell-скрипт, листинг которого можно найти ниже (В разделе «Инструменты»). Скрипт запускается с одним, двумя или тремя параметрами, каждый из которых представляет ключевое слово.
$ sh ~/Documents/LookSDKForSymbol.sh RingerSwitch
# часть результатов опущена
0000d738 S _kGSRingerSwitchCapability
Found in ./System/Library/PrivateFrameworks/GraphicsServices.framework/GraphicsServices
000038cc T _SBGetRingerSwitchState
0000370c T _SBGetRingerSwitchState
Found in ./System/Library/PrivateFrameworks/SpringBoardServices.framework/SpringBoardServices
Функция с названием SBGetRingerSwitchState
выглядела многообещающе.
Для получения нужного порта использовалась функция:
mach_port_t SBSSpringBoardServerPort();
из того же фреймворка.
Вот что получилось в итоге:
@implementation MuteSwitchTest
...
- (int) switchState
{
// Функции SBSSpringBoardServerPort и SBGetRingerSwitchState
// загружается при инициализации объекта MuteSwitchTest
mach_port_t port = SBSSpringBoardServerPort();
SInt32 state;
SBGetRingerSwitchState(port, &state);
return (int)state;
}
Пример 2:
Определение IMEI
IMEI (International Mobile Equipment Identity) — уникальный идентификационный
код, присваиваемый каждому телефону, своего рода MAC-адрес телефона (хотя MAC-адрес у телефона также есть)
Я уже и не помню, как я вышел проект Эрики Садун uidevice-extension, но по мере того, как я с ним разбирался он всё больше казался мне этакой программистской «золотой жилой».
Одна из категорий, UIDeviсe(IOKit_Extensions) содержит функции для определения IMEI. Я протестировал эти функции на iPhone 4 c iOS 5.1 и iPad c iOS 4.3, всё работало и я перешел к другим задачам. Но в ходе бета-тестирования выяснилось, что функция для определения IMEI не работает на новых устройствах: iPad 2, the new iPad и iPhone 4S. Для выяснения причин я отправился на StackOverflow, где мои опасения подтвердились. Поиски привели меня тогда к фреймворку под названием CoreTelephony.
$ nm -g ./CoreTelephony | grep -i imei
U _kCFAbsoluteTimeIntervalSince1970
00053b28 S _kCTMobileEquipmentInfoIMEI
00053ad4 S _kCTPostponementInfoIMEI
00053ac4 S _kCTPostponementStatusErrorDefaultIMEI
$ nm -g ./CoreTelephony | grep MobileEquipment
000260e4 T __CTServerConnectionCopyMobileEquipmentInfo
00053b34 S _kCTMobileEquipmentInfo1xIMSI
00053b20 S _kCTMobileEquipmentInfoCurrentMobileId
00053b24 S _kCTMobileEquipmentInfoCurrentSubscriberId
00053b40 S _kCTMobileEquipmentInfoERIVersion
00053b2c S _kCTMobileEquipmentInfoICCID
00053b28 S _kCTMobileEquipmentInfoIMEI
00053b30 S _kCTMobileEquipmentInfoIMSI
00053b38 S _kCTMobileEquipmentInfoMEID
00053b44 S _kCTMobileEquipmentInfoMIN
00053b3c S _kCTMobileEquipmentInfoPRLVersion
Можно предположить что функция (_CTServerConnectionCopyMobileEquipmentInfo(...)) возвращает словарь(CFDictionaryRef) c ключами вида kCTMobileEquipmentInfo* и соответствующими им значениями. К счастью, на этот раз мне не пришлось восстанавливать сигнатуру. Поиск в гугле по запросу _CTServerConnectionCopyMobileEquipmentInfo привел меня на эту страничку, и вскоре функция для определения IMEI была готова.
// Заголовочный файл с декларациями недокументированных функций и констант
#include "CoreTelephony.h"
...
NSString* CTGetIMEI
{
struct CTResult it;
NSMutableDictionary *dict;
CTServerConnectionRef conn;
conn = _CTServerConnectionCreate(kCFAllocatorDefault, ConnectionCallback, NULL);
_CTServerConnectionCopyMobileEquipmentInfo(&it, conn, &(CFMutableDictionaryRef)dict);
CFRelease(conn);
[dict autorelease];
return [dict objectForKey: kCTMobileEquipmentInfoIMEI];
}
Этот метод определения IMEI работает на всех устройствах.
Позже я нашел еще один метод определения IMEI (через lockdownd).
Пример 3:
Использование недокументированных оповещений: нажатия кнопок громкости.
Изначально я наивно полагал, что любая символьная константа, заканчивающаяся на «Notification» является названием системного оповещения и её можно использовать, просто зарегистрировав наблюдателя (observer) с помощью [NSNotificationCenter defaultCenter].
$ sh ~/Documents/LookSDKForSymbol.sh notification$ volume change
001dbe60 S _MPAVControllerVolumeDidChangeNotification
001dbe64 S _MPAVControllerVolumeMutedDidChangeNotification
001dc4f8 S _MPMusicPlayerControllerVolumeDidChangeNotification
001dc314 S _MPVolumeViewRouteButtonChangedNotification
001dc310 S _MPVolumeViewVisibilityChangedNotification
Found in ./System/Library/Frameworks/MediaPlayer.framework/MediaPlayer000d6d24 D _AVController_EffectiveVolumeDidChangeNotification
000d6d60 D _AVController_VolumeDidChangeNotification
000d6fec D _AVSystemController_CurrentRouteHasVolumeControlDidChangeNotification
000d6ffc D _AVSystemController_EffectiveVolumeDidChangeNotification
000d6fdc D _AVSystemController_SystemVolumeDidChangeNotification
Found in ./System/Library/PrivateFrameworks/Celestial.framework/Celestial
… и еще около десятка из других фреймворков
Написав тестовую программу, я принялся проверять, какие оповещения приходили в ответ на нажатия клавиш громкости.
Из составленного мной довольно большого списка опопвещений приходили только вот эти 2:
AVController_EffectiveVolumeDidChangeNotification
AVController_VolumeDidChangeNotification
Недостаток этих оповещений в том, что
1) Нельзя напрямую определить, какая из двух кнопок была нажата
2) Нельзя отследить, когда нажата и когда отпущена каждая из кнопок
Ищу по другим ключевым словам:
$ sh ~/Documents/LookSDKForSymbol.sh volume button
001b221c t -[UIApplication setWantsVolumeButtonEvents:]
003cce5c t _SBSetWantsVolumeButtonEvents$shim
0054478c S __UIApplicationVolumeDownButtonDownNotification
00544790 S __UIApplicationVolumeDownButtonUpNotification
00544784 S __UIApplicationVolumeUpButtonDownNotification
00544788 S __UIApplicationVolumeUpButtonUpNotification
Found in ./System/Library/Frameworks/UIKit.framework/UIKit
… и еще несколько десятков из разных фреймворкрв
Четыре оповещения из UIKit сработали не сразу: необходимо было подать связанную с ними команду.
[[UIApplication sharedApplication] setWantsVolumeButtonEvents: YES];
После этого стали приходить оповещения о нажатиях соответствующих кнопок.
Побочный эффект: вызов данной функции приводит к тому, что кнопки громкости больше не регулируют громкость, так что по завершении работы с кнопками следует вызвать
[[UIApplication sharedApplication] setWantsVolumeButtonEvents: NO];
Пример 4:
Использование недокументированных оповещений: отслеживание статуса SIM-карты
Работаем по проверенной схеме:
$ sh ~/Documents/LookSDKForSymbol.sh notification$ SIM
…
00052560 S _kCTSIMSupportSIMInsertionNotification
00052564 S _kCTSIMSupportSIMStatusChangeNotification
…
000525bc S _kCTSIMSupportSIMTrayStatusNotification
…
Found in ./System/Library/Frameworks/CoreTelephony.framework/CoreTelephony
…
Found in ./System/Library/PrivateFrameworks/FTServices.framework/FTServices
$
Наиболее подходящими мне показались оповещения под названиями:
1) kCTSIMSupportSIMInsertionNotification
2) kCTSIMSupportSIMStatusChangeNotification
3) kCTSIMSupportSIMTrayStatusNotification
Простейшая тестовая программа показала, что оповещения под названием (1) приходили только в момент вставки сим-карты (я мог бы догадаться и раньше по названию), (2) приходили именно тогда когда мне нужно (при вставке и вынимании), оповещения (3) не приходили вообще. Позже я узнал, что оповещения (3) относятся к специальному центру оповещений под названием CTTelephonyCenter. Об использовании CTTelephonyCenter можно прочитать здесь.
Оповещения о статусе SIM:
#include "CoreTelephony.h"
- (void) notificationCallback:(NSNotification)notification
{
...
}
- (void) startUpdateSIMStatus
{
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(notificationCallback:)
name:kCTSIMSupportSIMStatusChangeNotification
object:nil
];
}
- (void) stopUpdateSIMStatus
{
[[NSNotificationCenter defaultCenter]
removeObserver:self
name:kCTSIMSupportSIMStatusChangeNotification
object:nil];
}
Дополнения
1. Используем системные звуки в своем приложении
www.iphonedevwiki.net/index.php/AudioServices — здесь описаны недокументированные константы типа SystemSoundID для проигрывания стандартных коротких (< 30сек) звуков, таких как нажатие кнопок. Можно и самому их все найти, просто перебирая значения от 1000 в цикле.
Еще можно проигрывать стандартные рингтоны:
- (void) playDefaultRingTone
{
NSURL *defaultRingTone = [NSURL URLWithString:@"/System/Library/CoreServices/SpringBoard.app/ring.m4r"];
AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithContentsOfURL:defaultRingTone error:nil];
[player play];
}
2. Рекурсивный поиск по иерархии UIView
Как известно, у объектов класса UIView обычно есть родительский вид (superview) и могут быть дочерние виды (subviews). Верхушкой (как мы дальше увидим, верхушками) этой иерархии являются объект(ы) UIWindow. Что если пройти по всей иерархии? Тут есть тольк одна тонкость: как это не странно, в программе может быть больше одного объекта типа UIWindow.
Для получения __всех__ окон я использовал недокументированную функцию
+ [UIWindow allWindowsIncludingInternalWindows: onlyVisibleWindows:]
При помощи неё мне удалось обнаружить что в самом обычном приложении может быть до четырех окон! (UIWindow)
1) обычное окно программы
2) окно для статус-бара (его также можно получить при помощи — [UIApplication statusBarWindow] )
3) окно для UIAlertView (Родительский вид для объектов типа UIAlertView).
4) окно для экранной клавиатуры.
Какую пользу мы можем из этого извлечь?
Очевидно, что с объектами последних трех типов мы можем обращаться так же, как и с первым.
В частности, мы можем:
— изменять внешний вид UIAlertView, добавлять текстовые поля, переключатели и т.д.
— изменять внешний вид статус-бара, добавлять свои индикаторы и удалять стандартные.
— изменять внешний вид экранной клавиатуры: например, изменять вид кнопок, добавить свои кнопки, переключатели и т.д
Но это еще не все, есть также неожиданные побочные последствия. Часть графических элементов может быть напрямую связана с тем или иным более глубоким функционалом.
@interface UIStatusBarSignalStrengthItemView : UIStatusBarItemView {
@private
int _signalStrengthRaw;
int _signalStrengthBars;
BOOL _enableRSSI;
BOOL _showRSSI;
}
-(id)_stringForRSSI;
-(float)extraRightPadding;
-(void)touchesEnded:(id)ended withEvent:(id)event;
-(id)contentsImageForStyle:(int)style;
-(BOOL)updateForNewData:(id)newData actions:(int)actions;
@end
Так, например, получив доступ к UIStatusBarSignalStrengthItemView мы имеем возможность вполне легально получать значения RSSI (мощность принимаемого сигнала) сотовой сети, и отображать в любой удобной форме.
Полный листинг кода, рекурсивно описывающего все видимые объекты в программе. Вызывается через [UIView completeDescription]:
@interface UIView (RecursiveDescription)
- (void) recursiveDescription;
+ (void) completeDescription
@end
@implementation UIView (RecursiveDescription)
- (void) recursiveDescription
{
NSLog(@"______________________________________");
NSLog(@"%@", self);
NSArray *subviews_ = [self subviews];
if ([subviews_ count] > 0) {
for (UIView *subview_ in subviews_) {
[subview_ recursiveDescription];
}
}
}
- (void)completeDescription
{
NSArray *windows = [UIWindow allWindowsIncludingInternalWindows:YES onlyVisibleWindows:NO];
for (UIView *view in windows) {
[view recursiveDescription];
}
}
@end
3. Некоторые важные низкоуровневые подсистемы iOS
Подсистема MIG (MIG-subsystem, Mach Interface Generator) — интерфейс взаимодействия с ядром операционной системы (так называемый «Mach kernel»). Реализация MIG-subsystem для Mac OS X лежит где-то здесь: www.opensource.apple.com/source/xnu/xnu-1228.0.2/libsyscall/mach/. Смотри также список известных низкоуровневых сообщений: www.iphonedevwiki.net/index.php/MIG_subsystem.
IORegistry, I/O registry — реестр ввода-вывода; древовидная структура, описывающая аппаратное обеспечение iPhone и взаимодействие с аппаратными компонентами. Примеры использования IORegistry можно найти в проектах Эрики Садун (см. UIDevice-IOKitExtensions).
Инструменты
nm — UNIX-утилита, выводящая таблицу символов объектнового файла.
На основе nm я написал простой (и довольно тупой) bash-скрипт, который ищет по всем библиотекам и объектным файлам внутри iOS SDK.
LookSDKForSymbol.sh:
#!/bin/bash
SDK=/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS5.0.sdk
PrivateFrameworks=$SDK/System/Library/PrivateFrameworks
Frameworks=$SDK/System/Library/Frameworks
Lib=$SDK/usr/lib
cd $SDK
for i in $(find -f .); do
test -f $i && nm $i | c++filt | grep -i "$1" | grep -i "$2" | grep -i "$3" && echo "Found in $i
";
done
c++filt — восстановление (demangling) имён. Только для С++; имена objective-C и просто С идут сразу в человекочитаемом виде.
otool — стандартная утилита для анализа и дизассемблирования объектных файлов.
hexdump — дамп он дамп и есть :-)
class-dump-z — суперполезная утилита. Позволяет генерировать заголовочный файл из объектного файла. Описание всех структур, протоколов, классов, категорий, их методов, свойств и так далее.
Репозиторий на Google Code — здесь можно ознакомиться с более подробным описанием и скачать исходный код этого проекта.
Hex-Rays ARM Decompiler — плагин для HEX-Rays IDA, декомпилятор ARM кода. Официальный сайт.
Утилиты Эрики Садун — на сайте Эрики есть несколько полезных утилит, например, утилита для анализа оповещений.
Полезные ресурсы, источники
Wiki
www.iphonedevwiki.net/
Единственная известная мне wiki, относящаяся к jailbreak разработке. Хотя информация и устарела
(iOS 3.x, iOS 4.x), но все равно это крайне полезный ресурс.
Персоны
1. Джей Фримен (Jay Freeman, saurik)
www.saurik.com
2. Эрика Садун (Erica Sadun) — автор книг «The iPhone Developer's CookBook» и «The iOS 5 Developer's Cookbook: Core Concepts and Essential Recipes for iOS Programmers», а также многих полезных утилит. В её книгах описываются некоторые недокументированные возможности публичных фреймворков.
ericasadun.com/
Некоторые проекты Эрики Садун:
github.com/erica/iOS-5-Cookbook
github.com/erica/iphone-3.0-cookbook-
github.com/erica/uidevice-extension
3. KennyTM / networkpx — создатель class-dump-z, активный участник StackOverflow.com в вопросах, связанных с недокументированными API
networkpx.blogspot.com/
code.google.com/p/networkpx/
github.com/kennytm
stackoverflow.com/users/224671/kennytm
github.com/kennytm/iphone-private-frameworks
Информация сильно устарела (iOS 3).
Книги
«The iPhone Developer's CookBook» (также «The iOS 5 Developer's Cookbook: Core Concepts and Essential Recipes for iOS Programmers») — книга о разработке под iOS, с уклоном в недокументированные возможности iOS.
Русский перевод:
translated.by/you/iphone-developers-cookbook-development-native-applications-for-the-iphone/into-ru/trans/
Автор: Trahman