2ГИС давно хотел поделиться с пользователями айфонов своими знаниями о телефонных номерах компаний из справочника. Android-платформа давала такую возможность, а вот под iOS подходящего инструмента долго не было.

В июне мы ездили на WWDC 2016, и на одной из сессий ребята из Apple обмолвились, что наконец-то можно делать «gorgeous astonishment» — определитель номеров под iOS 10. Радости нашей не было предела, но до поры до времени: как Apple любит, фичу она предоставила с рядом ограничений.

Прототип


Первая «радость», с которой мы столкнулись — «богатая» документация, а именно:
> CXCallDirectoryExtensionContext

    @interface CXCallDirectoryExtensionContext : NSExtensionContext
    @property (nonatomic, weak, nullable) id<CXCallDirectoryExtensionContextDelegate> delegate;
    - (void)addBlockingEntryWithNextSequentialPhoneNumber:(CXCallDirectoryPhoneNumber)phoneNumber;
    - (void)addIdentificationEntryWithNextSequentialPhoneNumber:(CXCallDirectoryPhoneNumber)phoneNumber label:(NSString *)label;
    - (void)completeRequestWithCompletionHandler:(nullable void (^)(BOOL expired))completion;
    @end

> CXCallDirectoryManager

    @interface CXCallDirectoryManager : NSObject
    @property (readonly, class) CXCallDirectoryManager *sharedInstance;
    - (void)reloadExtensionWithIdentifier:(NSString *)identifier completionHandler:(nullable void (^)(NSError *_Nullable error))completion;
    - (void)getEnabledStatusForExtensionWithIdentifier:(NSString *)identifier completionHandler:(void (^)(CXCallDirectoryEnabledStatus enabledStatus, NSError *_Nullable error))completion;
    @end

И всё. Ну что ж, могло быть хуже.

Из этого видим, что dialer под iOS — это расширение приложения, которое крутится отдельным процессом, его можно перегрузить и получить его статус. Похоже на то, что нам нужно.
В самом же экстеншне можно добавить номера в виде «телефон/имя» и добавить номера для блокировки.

Первый прототип был готов за 30 минут. Один личный телефон, зашитый в экстеншн, один тестовый телефон добавлен в блокировку, всё завелось с первого раза, радости не было предела. Будущее выглядело крайне радужным — мы уже представляли, как всё это попадёт в ближайший релиз на следующий день.

Пока не столкнулись со второй «радостью»: мы не можем включить dialer из основного приложения. Нужно отправить пользователя глубоко в настройки, что явно не идёт на повышение конверсии этой фичи.

Потом начали добавлять пачку номеров и выяснилась третья «радость»: все номера нужно записать в базу до того, как они будут определены (это как раз знаменитая безопасность Apple — чтобы мы не получали доступ к входящему callerID). А наша база — это около 4 000 000 номеров с подписью. То есть 140 Мб текстовой информации, или 40 Мб, если пожать по самой жести, и всё это нужно каким-то образом доставить в расширение.

Вооружившись этим знанием, мы приготовили данные в виде «телефон/имя» и начали пилить уже более реальный прототип.

База данных


Сначала решили тупо добавить все номера, и вновь неожиданность — номера должны быть добавлены не абы как, а в порядке возрастания: 01, 02, 911 и т.д. В противном случае экстеншн падает. В первой бете 8 xcode экстеншен падал вообще без ошибок.

Далее выяснилось, что мы ограничены 1 999 999 номерами. Да, именно 1 999 999, а не 2 000 000, что тоже не совсем равняется нашим 4 000 000 номеров. Хотели сначала сделать три расширения, наполниться каждое до 1 999 999 номеров и в ус не дуть. Потом решили разделить по регионам: Москва + Питер, остальная Россия, зарубежка. Но от этого решения отказались, потому что нужно было придумать более сложную доставку и делать фичу еще менее стабильной, и работа нескольких одновременно работающих расширений тоже не была стабильной. Да и заставлять пользователя включать все три расширения тоже не хотелось. В итоге решили оставить только номера установленных у пользователя городов.

Поначалу хотели доставлять данные через SQLite. Собрали простую базу в 100 000 номеров из Новосибирска, написали логику работы с базой, запустили демопроект, и… ничего. Ошибок нет, всё ок, а номера не определяются.

Покопав это дело, выяснили, что при попытке вытащить данные из SQLite в ascending order база создаёт кеш на 30 Мб и экстеншн падает по памяти. Покопав форумы Apple, поняли, что лучше не вылезать за 5 Мб оперативной памяти. В итоге при объединённой базе для Москвы, Питера и ещё пары городов нужно будет сильно усложнять запросы к базе, строить хорошо оптимизированные по памяти и скорости фетчи, и усложнять процесс тестирования. Делать все это было совсем некогда, неохота, к тому же моих компетенций в околобазаданных технологий явно не хватало.

Запилили свой тупой, как бревно, формат данных в виде битовой последовательности:

[uint16_t: Размер блока][unsigned long long int:Phone][String:Name]

и очень простой парсер без заморочек:
    @interface DGSPhonesDataReader : NSObject
    /**
     Текущее значение телефона, пока не позван next, будет 0
     */
    @property (nonatomic, assign, readonly) unsigned long long int phone;
    /**
     Текущее значение имени, пока не позван next, будет nil
     */
    @property (nonatomic, copy, readonly, nullable) NSString *name;
    
    - (instancetype)initWithFilePath:(NSString *)path;
    - (BOOL)next;
    
    @end

    #import "DGSPhonesDataReader.h"
    
    @interface DGSPhonesDataReader ()
    @property (nonatomic, strong, readonly) NSData *data;
    @property (nonatomic, assign) NSUInteger location;
    @property (nonatomic, assign, readwrite) unsigned long long int phone;
    @property (nonatomic, copy, readwrite, nullable) NSString *name;
    @end
    
    @implementation DGSPhonesDataReader
    
    - (instancetype)initWithFilePath:(NSString *)path
    {
            self = [super init];
            if (self == nil) return nil;
    
            NSError *error = nil;
            _data = [NSData dataWithContentsOfFile:path options:NSDataReadingMappedIfSafe error:&error];
            _location = 0;
            if (_data == nil)
            {
                    NSLog(@"DGSPhonesDataReader data create error: %@", error);
            }
            return self;
    }
    
    - (BOOL)next
    {
            uint16_t blockLength;
            [self.data getBytes:&blockLength range:NSMakeRange(self.location, sizeof(blockLength))];
            self.location += sizeof(blockLength);
    
            unsigned long long int phone;
            NSUInteger textLength = blockLength - sizeof(phone);
            [self.data getBytes:&phone range:NSMakeRange(self.location, sizeof(phone))];
            self.phone = phone;
            self.location += sizeof(phone);
    
            uint8_t buffer[textLength];
            [self.data getBytes:buffer range:NSMakeRange(self.location, textLength)];
            self.name = [[NSString alloc] initWithBytes:buffer length:textLength encoding:NSUTF8StringEncoding];
            self.location += textLength;
    
            return self.location < self.data.length;
    }
    @end


Да, по идее нужно использовать кеш, читать блоком по 8 Кб и всякие такие дела. Но такой алгоритм пробегает по базе в 2 000 000 номеров за 10 секунд в отдельном системном процессе, не затрагивая никак основное приложение, притом происходит это один раз за обновление, поэтому решили сильно не заморачиваться с оптимизацией.

Ура! Теперь мы умеем безопасно парсить номера телефонов из базы, спокойно укладываясь в лимит 5 Мб памяти. Но время идёт, а фича всё ещё не готова.

Доставка данных


Дальше нужно было понять, как доставить эти данные в экстеншн, то есть, по сути, в отдельное приложение. Зашить их там не получится, так как пользователь скачивает новые регионы, удаляет старые, а ещё мы хотим всё обновлять, данные устаревают, добавляются новые, а мы же компания про точность и актуальность.

Оказалось, что за нас уже всё придумали и есть замечательная штука App Groups, которая позволяет шарить данные между двумя приложениями от одного разработчика.

Можно положить в основном приложении файл по пути:

    + (NSString *)extensionDataPath
    {
            return [[[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:[self extensionGroupName]].path stringByAppendingPathComponent:@"Dialer"];
    }

а в экстеншне достать его через:

    NSString *databasePath = [[DGSCallKitExtensionModel extensionDataPath] stringByAppendingPathComponent:manifest.databaseName];

Хоть проблем с доставкой не было никаких, и на том спасибо.

Дальше мы приготовили данные в нужном формате. Если не сильно углубляться, 500 Мб файл в формате .tsv нужно раскидать по 108 регионам, перегнать в бинарный формат, заархивировать и создать джобу на дженкинсе, чтобы не делать всё это руками и иметь готовую портянку данных для каждого релиза без особой боли. Короче, на это мы тоже потратили прилично времени — около 90% от всей разработки.

Встала задача доставить эти данные в телефон (вторые 90% разработки).

Сначала решили использовать технологию «On demand resources», а заодно и узнать, зачем нужна третья, вечно пустая вкладка в xcode — Resource Tags.



Эти ребята расскажут лучше:


Если коротко, Resource Tags для нас — это просто манна небесная (а именно Download Only On Demand). Она позволяет пометить некоторые ресурсы приложения тэгами, указать их тип, и при заливке приложения в стор он не будет включать их в бинарь. Потом их можно докачать при помощи NSBundleResourceRequest и получить через [NSBundle mainBundle]. То есть вообще не нужно пинать другие команды, придумывать, как их хранить и как доставлять до пользователя. А Apple сам хранит все данные + предоставляет очень адекватное API для их получения. Что сулило быструю интеграцию хотя бы здесь.

Но не всё оказалось так радужно: в первом релизе эта технология показала себя крайне паршиво, и примерно 20% пользователей тупо не смогли ничего скачать. Покопав форумы Apple, выяснили, что не у нас одних такая проблема, а они очень давно её не чинят и никак на неё не реагируют.

Resource Tags пришлось выпилить и доставлять данные другим способом. В итоге вшили данные в базу обновления городов. Теперь вместе с обновлением города пользователи получают новые базы номеров.

Всё впереди


Худо-бедно dialer попал в AppStore, и тут нас ждала четвёртая «радость».

После успешной установки мы удаляли базы, так как зачем хранить то, что уже и так находится в памяти телефона. Оказалось, не всё так просто: если пользователь зайдёт в настройки, выключит и включит экстеншн, то вместо того, чтобы просто включиться, экстеншн идёт по полному сценарию обновления. My bad, мы это не учли, и все, кто так делал, теряли базы без возможности их обновления. В следующей версии мы это оперативно поправили и теперь оставляем данные в телефоне, пока они ещё актуальны.

Мы постоянно получаем жалобы, что определитель не работает, или вопросы, как его включить. Пока, как промежуточный вариант, сделали отдельный пункт про определитель в настройках 2ГИС.

С iOS 10.3 Apple подкинула ещё проблем: если обновиться до этой версии, то определитель пропадает в настройках до тех пор, пока пользователь либо не переустановит приложение, либо не накатит обновление. Экстеншн в целом ведёт себя нестабильно. Периодически (по непонятным причинам и законам) он выключается или вовсе пропадает из настроек при обновлении. Иногда, в процессе обновления номеров, система молча прибивает экстеншен с кодами ошибок:

> CXErrorCodeCallDirectoryManagerErrorLoadingInterrupted;
> CXErrorCodeCallDirectoryManagerErrorUnknown.

Ещё в октябре мы создали пару радаров в Apple с просьбой дать нам ручку, чтобы позволить пользователям включить dialer из самого приложения, и по поводу баги с 10.3. Первый тикет Apple игнорирует с октября, а второй находится в ооочень длинной очереди.



Так что в ближайшее время мы вряд ли сможем сделать продукт лучше для пользователя.

Как всё это в итоге работает:

  1. Пользователь качает город/города;
  2. Из города достаётся база номеров в нашем формате;
  3. Смотрим все базы, которые установлены у пользователя (мы храним их в общем UserDefaults между экстеншном и основным приложением);
  4. У каждой базы есть хэш. Если хоть один хэш не совпал или появился новый, мы записываем все новые базы в общее хранилище и помечаем их как готовые к установке. Это нужно на случай, если пользователь не активировал экстеншн, а свернул приложение и включит его потом;
  5. Если экстеншн активен, перезагрузим его через:

      [[CXCallDirectoryManager sharedInstance] reloadExtensionWithIdentifier:bundleID completionHandler:^(NSError * _Nullable error) {}];
    

  6. В самом экстеншне, когда он получает:

        - (void)beginRequestWithExtensionContext:(CXCallDirectoryExtensionContext *)context
    

    мы смотрим, есть ли базы, готовые к установке. Если есть, мы пробегаем через все и добавляем номера через:

        [context addIdentificationEntryWithNextSequentialPhoneNumber:phone label:name];
    

  7. Помечаем базы как установленные;
  8. Повторяем процесс для каждого обновления;

В коде это выглядит примерно так:
    - (RACSignal *)reloadExtensionsIfNeeded
    {
            @weakify(self);
    
            if (![DGSCallKitFetchModel isExtensionAvailable] || self.manifests.count == 0) return [RACSignal empty];
    
            return [[[[[[[[[self fetchCanBeInstalledExtensionsRegionCodes]
                    filter:^BOOL(NSSet *regionCodes) {
                            return regionCodes.count > 0;
                    }]
                    deliverOn:[RACScheduler scheduler]]
                    flattenMap:^RACStream *(NSSet *regionCodes) {
                            @strongify(self);
    
                            return [RACSignal combineLatest:@[
                                    [self downloadDatabasesWithRegionCodesIfNeeded:regionCodes],
                                    [DGSCallKitFetchModel fetchExtensionEnabled]
                            ]];
                    }]
                    flattenMap:^RACStream *(RACTuple *t) {
                            @strongify(self);
    
                            RACTupleUnpack(NSSet *regionCodes, NSNumber *extensionEnabled) = t;
    
                            // Если дайлер не включен, то ничего не делаем
                            if (!extensionEnabled.boolValue) return [RACSignal empty];
    
                            // Если есть готовые базы, но они еще не установлены,
                            // то попробуем их установить в случае если пользователь разрешил дайлер в настройках,
                            // В остальных случаях не перезагружаем дайлер
                            if ([self shouldInstallDatabasesWithRegionCodes:regionCodes])
                            {
                                    return [RACSignal return:regionCodes];
                            }
                            else if ([self dialerEnabledWithRegionCodes:regionCodes])
                            {
                                    [self trackDialerInstalledEventWithRegionCodes:regionCodes];
                            }
                            return [RACSignal empty];
                    }]
                    flattenMap:^RACStream *(NSSet *regionCodes) {
                            @strongify(self);
    
                            return [self updateExtensionWithRegionCodes:regionCodes];
                    }]
                    doNext:^(NSSet *regionCodes) {
                            @strongify(self);
    
                            ULogInfo(@"Dialer extension installed with region codes: %@", regionCodes);
                            [self trackDialerInstalledEventWithRegionCodes:regionCodes];
                    }]
                    doError:^(NSError *error) {
                            @strongify(self);
    
                            ULogError(@"Dialer extension error: %@", error);
                            [self.analyticsSender trackEventWithCategory:kDGSCategoryDialer
                                                                                                      action:kDGSActionDialerFailed
                                                                                                       label:error.localizedDescription
                                                                                                       value:nil];
                    }]
                    doCompleted:^{
                            ULogInfo(@"Dialer extension reload completed signal");
                    }];
    }
    
    + (RACSignal *)fetchExtensionEnabled
    {
            NSString *bundleID = [DGSCallKitExtensionModel extensionBundleID];
            return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
                    [[CXCallDirectoryManager sharedInstance] getEnabledStatusForExtensionWithIdentifier:bundleID completionHandler:^(CXCallDirectoryEnabledStatus enabledStatus, NSError * _Nullable error) {
                            if (enabledStatus == CXCallDirectoryEnabledStatusEnabled)
                            {
                                    [subscriber sendNext:@YES];
                            }
                            else
                            {
                                    [subscriber sendNext:@NO];
                            }
                            [subscriber sendCompleted];
                    }];
                    return nil;
            }];
    }
    
    - (RACSignal *)updateExtensionWithRegionCodes:(NSSet<NSString *> *)regionCodes
    {
            ULogInfo(@"Reload dialer extension with tag: %@", regionCodes);
    
            NSString *bundleID = [DGSCallKitExtensionModel extensionBundleID];
            return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
                    [[CXCallDirectoryManager sharedInstance] reloadExtensionWithIdentifier:bundleID completionHandler:^(NSError * _Nullable error) {
                            if (error)
                            {
                                    [subscriber sendError:error];
                            }
                            else
                            {
                                    [subscriber sendNext:regionCodes];
                                    [subscriber sendCompleted];
                            }
                    }];
                    return nil;
            }];
    }


Основной проблемой при реализации этой фичи была подготовка данных и их доставка в приложение. Если зашить в экстеншн порядка 100 000 телефонов, то фичу можно сделать за час (при условии что они у вас есть).

Если нет данных в готовом формате и их нужно доставлять и обновлять хитрым образом, тогда на интеграцию этой фичи уйдёт уйма времени, а из-за сложности её включения пользователи, к сожалению, не скажут вам «большое спасибо». В большинстве отзывов будет что-то вроде «у меня не работает», «я скачал приложение, а ничего не определяет» и всё в таком духе.

Вместо заключения


На данный момент фича завершена, в ближайшее время планов по её доработке нет. Но всё ещё хочется сделать выборку по самым определяемым номерам — где-то в районе 100 000 номеров — и зашить их сразу в экстеншн, чтобы пользователи сразу получили минимальный функционал без необходимости скачивать регионы. Ещё у нас есть довольно много данных о «токсичных» номерах: коллекторские агентства, различного рода опросы, разные финансовые пирамиды и другие неугодные номера, на которые пожаловались пользователи Dialer на Android. Их мы тоже можем доставить отдельным пакетом всем желающим.



В целом хотелось чего-то более стабильного и более дружественного к пользователю, чтобы даже моя мама сама смогла его включить. В любом случае, как минимум 20 000 пользователей включили экстеншен, а это реальная польза и ощущение, что всё было не зря.
Поделиться с друзьями
-->

Комментарии (29)


  1. alexey-kirilenko
    02.03.2017 11:43
    +1

    пользовался одной из первых версию АОНа от 2GIS для android и замечал что ранее номер определялся в звонилке после звонка, а не во время звонка.
    Сейчас это так?


    1. teanet
      02.03.2017 14:03

      В iOS нет доступа к входящим звонкам, соответственно определить тоже никак нельзя кто звонил ни в приложении ни в экстеншене, можно только сказать какие телефоны можем определить. Пока что так.


      1. alexey-kirilenko
        02.03.2017 16:34

        т.е в Ios и Android АОН работает постфактум? если да то это печально.
        просто у стандартной звонилки Andorid 6.0 из коробки есть определение номера во время звонка. Обычные разработчики могут получить к этой фиче доступ?


        1. teanet
          02.03.2017 16:51
          +2

          1. это статья про определитель на iOS, я очень рад что на андроид все можно, на iOS разрешено только то что разрешено
          2. Мы не имеет доступа к входящим звонкам, мы всего лишь «учим» систему что номер 911 это служба спасения.
          3. Когда служба спасения звонит нам (приходит входящий вызов от 911) iOS смотрит базу всех номеров, которые были получены от экстеншенов (не обязательно нашего) если он находит номер 911, он вытаскивает этот номер из базы телефона (не расширения) и показывает его во время звонка (что видно на скриншоте).
          4. Обычные разработчики под iOS (я как обычный разработчик вам говорю) могут получить доступ к этой фиче, курите доки.


    1. Bull31
      04.03.2017 08:47

      сейчас определяется во время звонки даже если вы еще не подняли трубку при входящем.


  1. Dr_Zoidberg
    02.03.2017 13:29

    Насколько расширение жрет батарейку?


    1. teanet
      02.03.2017 14:06
      +1

      Расширение запускается один раз за одну версию, работает от 1 до 10 секунд, больше никак не поднимается, думаю жрет в районе 0.0035% (в среднем за 8 часов телефон разряжается 1 / (8 * 60 * 60) ? 0.000035) за каждое обновление приложения, порядок такой.


  1. ar0n
    02.03.2017 14:07

    Отдельное спасибо за инфу про ограничение оперативки в 5 мб! На днях закончил делать описанную фичу, но о таких ограничениях и подводных камнях даже не подозревал.
    Включение в глубине настроек — это конечно тихий ужас, очень надеюсь они это каким-то образом оптимизируют… =\


  1. V1tol
    02.03.2017 18:52

    А не пробовали поресёрчить в направлении CoreData + CloudKit? Возможно, решилась бы проблема с памятью (CoreData позволяет удобно батчить записи, хотя не уверен, что не съест те же 30 метров) и доставкой новых номеров пользователям.


    1. teanet
      02.03.2017 19:01
      +1

      Пробывали. В первую очередь, основная проблема отсортировать по номерам. Создается большой кэш, как не старался при базе в 1млн номеров вылазит за 5 метров только в путь. готовить правильно NSFetchRequest не хотелось (а если честно я CoreData прям не люблю)

      У нас уже есть готовая система доставки данных, которая легко держит нагрузки в 15млн пользователей, а после того как обожглись на On demand resources, не хотелось больше ничего иметь с эппловской системой доставки.

      Думаю это прям ок связка для баз в районе 100-300к и месячной аудитории такого же порядка.


  1. entze
    02.03.2017 19:03

    А как ведёт себя система, если включено 2 определителя от разных разработчиков? Не проверяли?


    1. teanet
      02.03.2017 19:11
      +2

      Ведет себя отлично, если в 2 определителях есть 2 одинаковых номера, то возьмет из того что выше по списку.

      Это была одна из гипотез как запихать 4 000 000+ млн номеров. Был прототип с 3 экстеншенами от одного приложения, все работало ок.

      Также проверяли как работает несколько разных приложений с CallKit экстеншенами, проблем тоже не было.


  1. Tangent
    03.03.2017 05:40

    При попытке включить в настройках «Блокировка и индентификация» появляется ошибка — «При попытке включить расширение 2ГИС произошла ошибка». Не подскажите, что в этом случае делать? (iOS 10.2.1)


    1. teanet
      03.03.2017 05:42

      Такое бывает, долгий дебаг экстеншена ничем не помог, просто падает какая то системная библиотека, помогает только включить/выключить пару раз, или переустановка приложения (без скачанного региона включится, но номера определять не будет). Других способов это вылечить мы не нашли (


  1. iFamily
    03.03.2017 05:42

    Специально установил 2gis, но не нашел в статье как включить экстеншн. Он сейчас доступен?


    1. teanet
      03.03.2017 05:47

      Нужно зайти в настройки > телефон > Блок. и индентификация вызова > Включить 2ГИС

      В картинках
      image
      image
      image


  1. k06a
    03.03.2017 08:35

    Простите, возможно я неправильно прочитал, но что за 10 сек на поиск по файлу, а как же бинарный поиск по отсортированным данным? Что за ограничение по оперативке, есть же маппинг файлов в память?


    1. teanet
      03.03.2017 10:46
      +1

      Мы не ищем по файлу, нам нужно записать в память телефона 100 000 — 2 000 000 номеров, в возрастающем порядке ровно один раз, именно так работает АОН на iOS 10. CallKit не дает на лету перехватить входящий номер, от слова никак.
      Для самого сложного случая нам нужно проитерировать 2 000 000 номеров. Итерация по всем номерам в экстеншене (а ему выделяется далеко не все системные ресурсы) и запись всего этого в память телефона занимает около 10 секунд на iPhone 5.


      1. k06a
        03.03.2017 11:18

        Спасибо, просто я еще не знаком с механизмом работы экстеншона этого типа. В статье хорошо было бы лишний раз подчеркнуть этот момент в самом начале.


      1. k06a
        03.03.2017 11:23
        +1

        В качестве оптимизации, предлагаю не копировать куски NSData в массивы а потом еще и в строки. Предлагаю сразу создавать NSString без копирования данных на исходном NSData через -initWithBytesNoCopy:length:encoding:freeWhenDone: https://developer.apple.com/reference/foundation/nsstring/1413830-initwithbytesnocopy?language=objc


        Учитывая, что -dataWithContentsOfFile:options:error: и так маппит файл в память – получение памяти сведется к минимуму вообще.


        1. teanet
          03.03.2017 15:06

          Да, ты прав, хоть это и не узкое горлышко, фикс выкачу)


  1. irishrover
    03.03.2017 12:11

    Уже дважды после обновления приложения 2ГИС невозможно включить флажок в настройках — iOS выдает ошибку о невозможности включения расширения. Причем и 2ГИС, и еще одного, которое никак с первым не связано. Помогает только переустановка iOS с последующим восстановлением профиля пользователя. Просто сброс и восстановление профиля не помогает.


  1. denisenkoaj
    04.03.2017 08:37

    Евгений, я пишу аналогичное приложение, только у меня под 10 миллионов номеров)) два вопроса:

    1) Скажите, происходит ли закачка номеров в экстеншн когда приложение свернуто в бэкграунд? У меня почему ранее при тестировании происходила, потом перестала. Не могу понять или не качественно изначально протестил или она не происходит.

    2) Можете дать пример файла с которого парсите номера? У меня приходятся архивированные файлы которые я распарсиваю, гоню в базу в Realm, в ней сортирую, и потом с экстеншна уже тяну отсортированные. По памяти все ок (в iOS 10.2 расширился лимит до 13-14 мб), но скорость этой операции жуть. Хочу взять ваш файл за образец.


  1. teanet
    04.03.2017 08:44

    denisenkoaj

    1. Да происходит, экстеншн это отдельное приложение которое крутиться отдельным процессом и не зависит от основного. Происходит закачка или нет можно проверить посмотрев системные логи, там будет что то вроде
    ...5000 phones added…
    ...5000 phones added…
    ...5000 phones added…

    2. Файл пожалуй не дам, но вот сниппет, который эти файлы создает. В статье есть пример парсера. По памяти там вообще нет просадки, а любая база делает сортированный прогон либо долгим либо прожорливым по памяти.

    Код
    @interface DGSPhonesDataWriter : NSObject
    - (void)writePhone:(unsigned long long int)phone name:(NSString *)name;
    - (void)saveWithPath:(NSString *)path;
    @end
    


    #import "DGSPhonesDataWriter.h"
    
    @implementation DGSPhonesDataWriter {
    	NSMutableData *_data;
    }
    
    - (instancetype)init {
    	self = [super init];
    	if (self == nil) return nil;
    	_data = [NSMutableData data];
    	return self;
    }
    
    - (void)writePhone:(unsigned long long int)phone name:(NSString *)name {
    	NSMutableData *localData = [NSMutableData data];
    	[localData appendBytes:&phone length:sizeof(unsigned long long int)];
    	[localData appendData:[name dataUsingEncoding:NSUTF8StringEncoding]];
    	uint16_t len = (uint16_t)localData.length;
    	[_data appendBytes:&len length:sizeof(len)];
    	[_data appendData:localData];
    }
    
    - (void)saveWithPath:(NSString *)path {
    	[_data writeToFile:path atomically:YES];
    }
    
    @end
    


    1. denisenkoaj
      04.03.2017 10:31

      teanet Спасибо, супер! А как вы доступ получаете к логам?
      Потому что NSLog от экстеншна в консоль не пишет, я отдельно сделал логирование через userDefaults — через записи них у меня экстеншн сообщает свой статус основному приложению.


      1. teanet
        04.03.2017 10:41

        Мы забили на логи, потому что на процесс обновления мы повлиять не можем, а успешный сценарий проходит всегда, так как данные заранее подготовлены.

        Мы смотрели в сторону дарвиновских нотификаций, но из за нехватки времени и малой приоритетности этой фичи, не дожали эту тему.

        Если Apple выкатит более вменяемый api для включения из приложения, вернемся к этой теме.


  1. Phizio
    05.03.2017 09:35

    Интересно, не думали о сборе фидбэка с тех номеров, которые не определились? я б честно говоря с удовольствием после отшивания очередного банка, предлагающего кредит, в открывающейся модалке (может еще как-то, чтобы не сильно мешало в других ситуациях) отписывал что-то типа 'в** банк спам'. Понятно, что будет много ложной инфы, но можно на big data усреднять, наверное. Итог — рост и актуализация базы за счет n% сознательных юзеров.


    1. teanet
      05.03.2017 12:06

      Повторюсь, мы не имеем доступа к входящим звонкам. Никак, от слова вообще. Есть api для определения факта того что был входящий звонок, но номер мы получить не сможем. Плюс, не думаю что нашим пользователям понравится всплывающее окно каждый раз после того как они получили входящий звонок.

      И на всякий случай еще раз
      image


  1. cypok
    06.03.2017 05:52

    Только сегодня узнал про эту фичу, странно, что никаких анонсов нигде не видел, они были?
    Выглядит полезным, включил, потестим!