ScanOperation

Off load scan task to background thread, blazingly fast app launch
speed
This commit is contained in:
Chris Li 2016-07-13 15:44:07 -04:00
parent c1edab0f05
commit c50c7e8c6e
11 changed files with 212 additions and 141 deletions

View File

@ -57,7 +57,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, OperationQueueDelegate {
func applicationDidBecomeActive(application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
NSTimer.scheduledTimerWithTimeInterval(60.0, target: self, selector: #selector(AppDelegate.recordActiveSession), userInfo: nil, repeats: false)
ZimMultiReader.sharedInstance.scan()
}
func applicationWillTerminate(application: UIApplication) {

View File

@ -56,6 +56,7 @@ class MainVC: UIViewController {
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
// showGetStarted()
}

View File

@ -45,7 +45,8 @@ extension MainVC: LPTBarButtonItemDelegate, TableOfContentsDelegate, ZimMultiRea
// MARK: - ZimMultiReaderDelegate
func firstBookAdded(id: ZimID) {
func firstBookAdded() {
guard let id = ZimMultiReader.sharedInstance.readers.keys.first else {return}
loadMainPage(id)
}

View File

@ -163,7 +163,6 @@ class Network: NSObject, NSURLSessionDelegate, NSURLSessionDownloadDelegate, NSU
let bookDownloadTask = book.downloadTask else {return}
context.performBlockAndWait { () -> Void in
book.isLocal = true
self.context.deleteObject(bookDownloadTask)
}

View File

@ -348,6 +348,7 @@
<webView hidden="YES" opaque="NO" contentMode="scaleToFill" layoutMarginsFollowReadableWidth="YES" translatesAutoresizingMaskIntoConstraints="NO" id="hIy-lu-2F6">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<dataDetectorType key="dataDetectorTypes"/>
</webView>
<visualEffectView hidden="YES" opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="r0g-Y2-P3v">
<rect key="frame" x="0.0" y="600" width="600" height="390"/>

View File

@ -180,7 +180,7 @@
97E609F11D103DED00EBCB9D /* NotificationCenter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97E609F01D103DED00EBCB9D /* NotificationCenter.framework */; };
97E609F41D103DED00EBCB9D /* TodayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97E609F31D103DED00EBCB9D /* TodayViewController.swift */; };
97E609F71D103DED00EBCB9D /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97E609F51D103DED00EBCB9D /* MainInterface.storyboard */; };
97E60A021D10423A00EBCB9D /* ShadowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97E60A011D10423A00EBCB9D /* ShadowView.swift */; };
97E60A021D10423A00EBCB9D /* ShadowViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97E60A011D10423A00EBCB9D /* ShadowViews.swift */; };
97E60A061D10504000EBCB9D /* LibraryBackupTBVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97E60A051D10504000EBCB9D /* LibraryBackupTBVC.swift */; };
97E850CB1D2DA5B300A9F688 /* About.html in Resources */ = {isa = PBXBuildFile; fileRef = 97E850CA1D2DA5B300A9F688 /* About.html */; };
97E891691CA976E90001CA32 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97E891681CA976E90001CA32 /* FileManager.swift */; };
@ -432,7 +432,7 @@
97E609F31D103DED00EBCB9D /* TodayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayViewController.swift; sourceTree = "<group>"; };
97E609F61D103DED00EBCB9D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
97E609F81D103DED00EBCB9D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
97E60A011D10423A00EBCB9D /* ShadowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowView.swift; sourceTree = "<group>"; };
97E60A011D10423A00EBCB9D /* ShadowViews.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowViews.swift; sourceTree = "<group>"; };
97E60A051D10504000EBCB9D /* LibraryBackupTBVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LibraryBackupTBVC.swift; path = "Kiwix-iOS/Controller/LibraryBackupTBVC.swift"; sourceTree = SOURCE_ROOT; };
97E850CA1D2DA5B300A9F688 /* About.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; name = About.html; path = Kiwix/HelpDocuments/About.html; sourceTree = SOURCE_ROOT; };
97E891681CA976E90001CA32 /* FileManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FileManager.swift; path = Kiwix/FileManager.swift; sourceTree = "<group>"; };
@ -702,7 +702,7 @@
971A10281D022AD5007FC62C /* LTBarButtonItem.swift */,
971A10291D022AD5007FC62C /* RefreshHUD.swift */,
971A102A1D022AD5007FC62C /* SearchBar.swift */,
97E60A011D10423A00EBCB9D /* ShadowView.swift */,
97E60A011D10423A00EBCB9D /* ShadowViews.swift */,
);
path = View;
sourceTree = "<group>";
@ -1580,7 +1580,7 @@
971A106F1D022E62007FC62C /* DownloadProgress.swift in Sources */,
971A102E1D022AD5007FC62C /* TableViewCells.swift in Sources */,
971A105A1D022DAD007FC62C /* LibraryLocalTBVC.swift in Sources */,
97E60A021D10423A00EBCB9D /* ShadowView.swift in Sources */,
97E60A021D10423A00EBCB9D /* ShadowViews.swift in Sources */,
9779A1CC1D34225E0071EFAB /* SearchOperation.swift in Sources */,
971A10671D022E0A007FC62C /* MainVCDelegates.swift in Sources */,
978C58981C1CD86E0077AE47 /* Book.swift in Sources */,

View File

@ -9,29 +9,13 @@
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "Kiwix-iOS/Model/KiwixURLProtocol.swift"
timestampString = "489951010.904281"
filePath = "Kiwix/ZimMultiReader/ZimMultiReader.swift"
timestampString = "490129771.550315"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "24"
endingLineNumber = "24"
landmarkName = "startLoading()"
landmarkType = "5">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "Kiwix-iOS/Model/KiwixURLProtocol.swift"
timestampString = "489951012.827944"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "38"
endingLineNumber = "38"
landmarkName = "startLoading()"
startingLineNumber = "71"
endingLineNumber = "71"
landmarkName = "directoryMonitorDidObserveChange()"
landmarkType = "5">
</BreakpointContent>
</BreakpointProxy>

View File

@ -14,8 +14,6 @@ import CoreData
import AppKit
#endif
class Book: NSManagedObject {
// MARK: - Add Book
@ -95,6 +93,20 @@ class Book: NSManagedObject {
return fetch(fetchRequest, type: Book.self, context: context) ?? [Book]()
}
class func fetchLocal(context: NSManagedObjectContext) -> [ZimID: Book] {
let fetchRequest = NSFetchRequest(entityName: "Book")
let predicate = NSPredicate(format: "isLocal = true")
fetchRequest.predicate = predicate
let localBooks = fetch(fetchRequest, type: Book.self, context: context) ?? [Book]()
var books = [ZimID: Book]()
for book in localBooks {
guard let id = book.id else {continue}
books[id] = book
}
return books
}
class func fetch(id: String, context: NSManagedObjectContext) -> Book? {
let fetchRequest = NSFetchRequest(entityName: "Book")
fetchRequest.predicate = NSPredicate(format: "id = %@", id)

View File

@ -6,8 +6,150 @@
// Copyright © 2016 Chris. All rights reserved.
//
import UIKit
import CoreData
import PSOperations
class ScanLocalBookOperation: NSObject {
class ScanLocalBookOperation: Operation {
private let context: NSManagedObjectContext
private var firstBookAdded = false
private var lastZimFileURLSnapshot: Set<NSURL>
private var currentZimFileURLSnapshot = Set<NSURL>()
private let lastIndexFolderURLSnapshot: Set<NSURL>
private var currentIndexFolderURLSnapshot = Set<NSURL>()
private var completionHandler: ((currentZimFileURLSnapshot: Set<NSURL>, currentIndexFolderURLSnapshot: Set<NSURL>, firstBookAdded: Bool) -> Void)
init(lastZimFileURLSnapshot: Set<NSURL>, lastIndexFolderURLSnapshot: Set<NSURL>,
completionHandler: ((currentZimFileURLSnapshot: Set<NSURL>, currentIndexFolderURLSnapshot: Set<NSURL>, firstBookAdded: Bool) -> Void)) {
self.context = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
context.parentContext = NSManagedObjectContext.mainQueueContext
context.mergePolicy = NSOverwriteMergePolicy
self.lastZimFileURLSnapshot = lastZimFileURLSnapshot
self.lastIndexFolderURLSnapshot = lastIndexFolderURLSnapshot
self.completionHandler = completionHandler
super.init()
name = String(self)
}
override func execute() {
defer {finish()}
currentZimFileURLSnapshot = ScanLocalBookOperation.getCurrentZimFileURLsInDocDir()
currentIndexFolderURLSnapshot = ScanLocalBookOperation.getCurrentIndexFolderURLsInDocDir()
let zimFileHasChanges = lastZimFileURLSnapshot != currentZimFileURLSnapshot
let indexFolderHasDeletions = lastIndexFolderURLSnapshot.subtract(currentIndexFolderURLSnapshot).count > 0
guard zimFileHasChanges || indexFolderHasDeletions else {return}
if indexFolderHasDeletions {
lastZimFileURLSnapshot.removeAll()
}
updateReaders()
updateCoreData()
}
override func finished(errors: [NSError]) {
context.performBlockAndWait {self.context.saveIfNeeded()}
NSManagedObjectContext.mainQueueContext.performBlockAndWait {NSManagedObjectContext.mainQueueContext.saveIfNeeded()}
NSOperationQueue.mainQueue().addOperationWithBlock {
self.completionHandler(currentZimFileURLSnapshot: self.currentZimFileURLSnapshot,
currentIndexFolderURLSnapshot: self.currentIndexFolderURLSnapshot, firstBookAdded: self.firstBookAdded)
}
}
private func updateReaders() {
let addedZimFileURLs = currentZimFileURLSnapshot.subtract(lastZimFileURLSnapshot)
let removedZimFileURLs = lastZimFileURLSnapshot.subtract(currentZimFileURLSnapshot)
guard addedZimFileURLs.count > 0 || removedZimFileURLs.count > 0 else {return}
ZimMultiReader.sharedInstance.removeReaders(removedZimFileURLs)
ZimMultiReader.sharedInstance.addReaders(addedZimFileURLs)
}
private func updateCoreData() {
let localBooks = Book.fetchLocal(context)
let zimReaderIDs = Set(ZimMultiReader.sharedInstance.readers.keys)
let addedZimFileIDs = zimReaderIDs.subtract(Set(localBooks.keys))
let removedZimFileIDs = Set(localBooks.keys).subtract(zimReaderIDs)
for id in removedZimFileIDs {
guard let book = localBooks[id] else {continue}
if let _ = book.meta4URL {
book.isLocal = false
} else {
context.deleteObject(book)
}
}
for id in addedZimFileIDs {
guard let reader = ZimMultiReader.sharedInstance.readers[id] else {return}
let book: Book? = {
let book = Book.fetch(id, context: NSManagedObjectContext.mainQueueContext)
return book ?? Book.add(reader.metaData, context: NSManagedObjectContext.mainQueueContext)
}()
book?.isLocal = true
book?.hasIndex = reader.hasIndex()
book?.hasPic = !reader.fileURL.absoluteString.containsString("nopic")
}
for (id, book) in localBooks {
guard !context.deletedObjects.contains(book) else {continue}
guard let reader = ZimMultiReader.sharedInstance.readers[id] else {return}
book.hasIndex = reader.hasIndex()
}
if localBooks.count == 0 && addedZimFileIDs.count == 1 {
firstBookAdded = true
}
}
// MARK: - Helper
private class func getCurrentZimFileURLsInDocDir() -> Set<NSURL> {
let fileURLs = FileManager.contentsOfDirectory(FileManager.docDirURL) ?? [NSURL]()
var zimURLs = Set<NSURL>()
for url in fileURLs {
do {
var isDirectory: AnyObject? = nil
try url.getResourceValue(&isDirectory, forKey: NSURLIsDirectoryKey)
if let isDirectory = (isDirectory as? NSNumber)?.boolValue {
if !isDirectory {
guard let pathExtension = url.pathExtension?.lowercaseString else {continue}
guard pathExtension.containsString("zim") else {continue}
zimURLs.insert(url)
}
}
} catch {
continue
}
}
return zimURLs
}
private class func getCurrentIndexFolderURLsInDocDir() -> Set<NSURL> {
let fileURLs = FileManager.contentsOfDirectory(FileManager.docDirURL) ?? [NSURL]()
var folderURLs = Set<NSURL>()
for url in fileURLs {
do {
var isDirectory: AnyObject? = nil
try url.getResourceValue(&isDirectory, forKey: NSURLIsDirectoryKey)
if let isDirectory = (isDirectory as? NSNumber)?.boolValue {
if isDirectory {
guard let pathExtension = url.pathExtension?.lowercaseString else {continue}
guard pathExtension == "idx" else {continue}
folderURLs.insert(url)
}
}
} catch {
continue
}
}
return folderURLs
}
}

View File

@ -11,26 +11,21 @@ import PSOperations
class ZimMultiReader: NSObject, DirectoryMonitorDelegate {
static let sharedInstance = ZimMultiReader()
let searchQueue = OperationQueue()
weak var delegate: ZimMultiReaderDelegate?
private weak var scanOperation: ScanLocalBookOperation?
private(set) var readers = [ZimID: ZimReader]() {
didSet {
if readers.count == 1 {
guard let id = readers.keys.first else {return}
delegate?.firstBookAdded(id)
}
}
}
let searchQueue = OperationQueue()
private(set) var isScanning = false
private(set) var readers = [ZimID: ZimReader]()
private let monitor = DirectoryMonitor(URL: FileManager.docDirURL)
private var zimURLs = Set<NSURL>()
private var zimAdded = Set<NSURL>()
private var zimRemoved = Set<NSURL>()
private var indexFolders = Set<NSURL>()
private var lastZimFileURLSnapshot = Set<NSURL>()
private var lastIndexFolderURLSnapshot = Set<NSURL>()
override init() {
super.init()
startScan()
monitor.delegate = self
monitor.startMonitoring()
}
@ -39,117 +34,54 @@ class ZimMultiReader: NSObject, DirectoryMonitorDelegate {
monitor.stopMonitoring()
}
// MARK: - DirectoryMonitorDelegate
func directoryMonitorDidObserveChange() {
scan()
}
// MARK: - Scan
func scan() {
/*
If list of idx folders changes, reinitialize all zim readers,
because currently ZimMultiReader cannot find out which ZimReader's index folder is added or deleted
Note: when a idx folder is added, the content of that idx folder will not finish copying, which makes it meanless to detect idx folder addition.
Because, with a incompletely copied idx folder, the xapian initializer is guranteed to fail. So here only check for idx folder deletion.
If user added a idx folder, he or she needs to manaually call rescan.
*/
let newIndexFolders = Set(indexFolderURLsInDocDir)
let deletedIdxFolder = indexFolders.subtract(newIndexFolders)
// Check for idx folder deletion
if deletedIdxFolder.count > 0 {
zimURLs.removeAll()
}
indexFolders = newIndexFolders
// Below are the lines required when not considering idx folders, aka only detect zim files
let newZimURLs = Set(zimFileURLsInDocDir)
zimAdded = newZimURLs.subtract(zimURLs)
zimRemoved = zimURLs.subtract(newZimURLs)
removeOld()
addNew()
zimAdded.removeAll()
zimRemoved.removeAll()
zimURLs = newZimURLs
}
private func removeOld() {
for (id, reader) in readers {
guard zimRemoved.contains(reader.fileURL) else {continue}
readers[id] = nil
guard let book = Book.fetch(id, context: NSManagedObjectContext.mainQueueContext) else {return}
if let _ = book.meta4URL {
book.isLocal = false
} else {
NSManagedObjectContext.mainQueueContext.deleteObject(book)
func startScan() {
isScanning = true
let scanOperation = ScanLocalBookOperation(lastZimFileURLSnapshot: lastZimFileURLSnapshot, lastIndexFolderURLSnapshot: lastIndexFolderURLSnapshot) { (currentZimFileURLSnapshot, currentIndexFolderURLSnapshot, firstBookAdded) in
self.lastZimFileURLSnapshot = currentZimFileURLSnapshot
self.lastIndexFolderURLSnapshot = currentIndexFolderURLSnapshot
self.isScanning = false
if firstBookAdded {
self.delegate?.firstBookAdded()
}
}
GlobalOperationQueue.sharedInstance.addOperation(scanOperation)
self.scanOperation = scanOperation
}
private func addNew() {
for url in zimAdded {
// MARK: - Reader Addition / Deletion
func addReaders(urls: Set<NSURL>) {
for url in urls {
guard let reader = ZimReader(ZIMFileURL: url) else {continue}
let id = reader.getID()
readers[id] = reader
let book: Book? = {
let book = Book.fetch(id, context: NSManagedObjectContext.mainQueueContext)
return book ?? Book.add(reader.metaData, context: NSManagedObjectContext.mainQueueContext)
}()
book?.isLocal = true
book?.hasIndex = reader.hasIndex()
book?.hasPic = !reader.fileURL.absoluteString.containsString("nopic")
}
}
private var zimFileURLsInDocDir: [NSURL] {
let fileURLs = FileManager.contentsOfDirectory(FileManager.docDirURL) ?? [NSURL]()
var zimURLs = [NSURL]()
for url in fileURLs {
do {
var isDirectory: AnyObject? = nil
try url.getResourceValue(&isDirectory, forKey: NSURLIsDirectoryKey)
if let isDirectory = (isDirectory as? NSNumber)?.boolValue {
if !isDirectory {
guard let pathExtension = url.pathExtension?.lowercaseString else {continue}
guard pathExtension.containsString("zim") else {continue}
zimURLs.append(url)
}
}
} catch {
continue
}
func removeReaders(urls: Set<NSURL>) {
for (id, reader) in readers {
guard urls.contains(reader.fileURL) else {continue}
readers[id] = nil
}
return zimURLs
}
private var indexFolderURLsInDocDir: [NSURL] {
let fileURLs = FileManager.contentsOfDirectory(FileManager.docDirURL) ?? [NSURL]()
var folderURLs = [NSURL]()
for url in fileURLs {
do {
var isDirectory: AnyObject? = nil
try url.getResourceValue(&isDirectory, forKey: NSURLIsDirectoryKey)
if let isDirectory = (isDirectory as? NSNumber)?.boolValue {
if isDirectory {
guard let pathExtension = url.pathExtension?.lowercaseString else {continue}
guard pathExtension == "idx" else {continue}
folderURLs.append(url)
}
}
} catch {
continue
}
}
return folderURLs
// MARK: - DirectoryMonitorDelegate
func directoryMonitorDidObserveChange() {
startScan()
}
// MARK: - Search
func startSearch(searchOperation: SearchOperation) {
if let scanOperation = scanOperation {
searchOperation.addDependency(scanOperation)
}
searchQueue.addOperation(searchOperation)
}
// MARK: Search (Old)
func search(searchTerm: String, zimFileID: String) -> [(id: String, articleTitle: String)] {
var resultTuples = [(id: String, articleTitle: String)]()
let firstCharRange = searchTerm.startIndex...searchTerm.startIndex
@ -200,6 +132,6 @@ class ZimMultiReader: NSObject, DirectoryMonitorDelegate {
}
protocol ZimMultiReaderDelegate: class {
func firstBookAdded(id: ZimID)
func firstBookAdded()
}