├── .gitignore ├── BSBacktraceLogger └── BSBacktraceLogger │ ├── BSBacktraceLogger.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── BSBacktraceLogger │ ├── BSBacktraceLogger.h │ ├── BSBacktraceLogger.m │ └── Info.plist ├── Example ├── Example.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcuserdata │ │ └── jiaxin.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist ├── Example.xcworkspace │ └── contents.xcworkspacedata └── Example │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── BaseNavigationViewController.swift │ ├── CrashTestViewController.swift │ ├── Info.plist │ ├── MySoldiers │ └── ServerEnvironmentSoldier.swift │ ├── NetworkTestViewController.swift │ ├── PerformanceTestViewController.swift │ ├── SanboxTestViewController.swift │ ├── UIWebViewViewController.swift │ ├── ViewController.swift │ └── lufei.jpg ├── JXCaptain.podspec ├── JXCaptain.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcuserdata │ └── jiaxin.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── JXCaptain ├── Info.plist └── JXCaptain.h ├── LICENSE ├── README.md ├── Resources └── Resource.bundle │ ├── JXCaptain_icon_anr.png │ ├── JXCaptain_icon_app_info.png │ ├── JXCaptain_icon_app_user_defaults.png │ ├── JXCaptain_icon_cpu.png │ ├── JXCaptain_icon_crash.png │ ├── JXCaptain_icon_fps.png │ ├── JXCaptain_icon_h5.png │ ├── JXCaptain_icon_memory.png │ ├── JXCaptain_icon_network.png │ ├── JXCaptain_icon_sanbox.png │ └── JXCaptain_icon_shield.png └── Sources ├── Captain.swift ├── CaptainDefines.swift ├── Manager ├── File │ ├── ANRFileManager.swift │ ├── CrashFileManager.swift │ └── SoldierFileManager.swift ├── Image │ └── ImageManager.swift └── NetworkManager │ └── NetworkManager.swift ├── Soldiers ├── ANR │ ├── ANRMonitor.swift │ ├── ANRPingThread.swift │ └── ANRSoldier.swift ├── AppInfo │ ├── AppInfoManager.swift │ └── AppInfoSoldier.swift ├── CPU │ ├── CPUMonitor.swift │ └── CPUSoldier.swift ├── Crash │ ├── CrashSignalExceptionHandler.swift │ ├── CrashSoldier.swift │ └── CrashUncaughtExceptionHandler.swift ├── FPS │ ├── FPSMonitor.swift │ └── FPSSoldier.swift ├── Memory │ ├── MemoryMonitor.swift │ └── MemorySoldier.swift ├── NetworkObserver │ ├── JXCaptainURLProtocol.swift │ └── NetworkObserverSoldier.swift ├── SanboxBrowser │ └── SanboxBrowserSoldier.swift ├── UserDefaults │ └── UserDefaultsSoldier.swift └── WebsiteEntry │ └── WebsiteEntrySoldier.swift ├── UI ├── ANR │ ├── ANRDashboardViewController.swift │ └── ANRListViewController.swift ├── AppInfo │ └── AppInfoViewController.swift ├── CPU │ └── CPUDashboardViewController.swift ├── Common │ ├── DashboardCell.swift │ ├── FileListViewController.swift │ ├── MonitorConsoleLabel.swift │ └── MonitorListWindow.swift ├── Crash │ ├── CrashDashboardViewController.swift │ └── CrashListViewController.swift ├── FPS │ └── FPSDashboardViewController.swift ├── Main │ ├── BaseTableViewController.swift │ ├── BaseViewController.swift │ ├── CaptainFloatingViewController.swift │ ├── CaptainFloatingWindow.swift │ └── SoldierListViewController.swift ├── Memory │ └── MemoryDashboardViewController.swift ├── Model │ ├── NetworkFlowDetailCellModel.swift │ ├── NetworkFlowDetailSectionModel.swift │ ├── NetworkFlowModel.swift │ ├── SanboxModel.swift │ ├── SoldierListSectionModel.swift │ └── UserDefaultsKeyValueModel.swift ├── NetworkObserver │ ├── NetworkFlowDetailViewController.swift │ ├── NetworkFlowListViewController.swift │ ├── NetworkFlowResponseDataDetailViewController.swift │ └── NetworkObserverDashboardViewController.swift ├── SanboxBrowser │ ├── ExcelView.swift │ ├── JXDatabaseConnector.swift │ ├── JXFileBrowserController.swift │ ├── JXFilePreviewViewController.swift │ ├── JXTableContentViewController.swift │ ├── JXTableListViewController.swift │ └── JXTextPreviewViewController.swift ├── UserDefaults │ ├── UserDefaultsKeyValueDetailViewController.swift │ └── UserDefaultsKeyValuesListViewController.swift └── WebsiteEntry │ ├── WebDetailViewController.swift │ └── WebsiteEntryViewController.swift └── UserDefaults+Access.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots/**/*.png 68 | fastlane/test_output 69 | -------------------------------------------------------------------------------- /BSBacktraceLogger/BSBacktraceLogger/BSBacktraceLogger.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /BSBacktraceLogger/BSBacktraceLogger/BSBacktraceLogger/BSBacktraceLogger.h: -------------------------------------------------------------------------------- 1 | // 2 | // BSBacktraceLogger.h 3 | // BSBacktraceLogger 4 | // 5 | // Created by 张星宇 on 16/8/27. 6 | // Copyright © 2016年 bestswifter. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #define BSLOG NSLog(@"%@",[BSBacktraceLogger bs_backtraceOfCurrentThread]); 12 | #define BSLOG_MAIN NSLog(@"%@",[BSBacktraceLogger bs_backtraceOfMainThread]); 13 | #define BSLOG_ALL NSLog(@"%@",[BSBacktraceLogger bs_backtraceOfAllThread]); 14 | 15 | @interface BSBacktraceLogger : NSObject 16 | 17 | + (NSString *)bs_backtraceOfAllThread; 18 | + (NSString *)bs_backtraceOfCurrentThread; 19 | + (NSString *)bs_backtraceOfMainThread; 20 | + (NSString *)bs_backtraceOfNSThread:(NSThread *)thread; 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /BSBacktraceLogger/BSBacktraceLogger/BSBacktraceLogger/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/xcuserdata/jiaxin.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Example.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 1 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Example/Example.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Example/Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Example 4 | // 5 | // Created by jiaxin on 2019/8/20. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | var window: UIWindow? 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // 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. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/Example/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /Example/Example/BaseNavigationViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseNavigationViewController.swift 3 | // Example 4 | // 5 | // Created by jiaxin on 2019/9/3. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class BaseNavigationViewController: UINavigationController { 12 | 13 | override var childForStatusBarStyle: UIViewController? { 14 | return topViewController 15 | } 16 | 17 | override var childForStatusBarHidden: UIViewController? { 18 | return topViewController 19 | } 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | 24 | // Do any additional setup after loading the view. 25 | } 26 | 27 | 28 | } 29 | -------------------------------------------------------------------------------- /Example/Example/CrashTestViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CrashTestViewController.swift 3 | // Example 4 | // 5 | // Created by jiaxin on 2019/8/23. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class CrashTestViewController: UITableViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | 16 | title = "Crash测试" 17 | tableView.tableFooterView = UIView() 18 | } 19 | 20 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 21 | if indexPath.row == 0 { 22 | let ocArray = NSArray() 23 | print(ocArray[1]) 24 | }else if indexPath.row == 1 { 25 | let someVaule: Int? = nil 26 | print(someVaule!) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Example/Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | View controller-based stats bar appearance 38 | 39 | NSAppTransportSecurity 40 | 41 | NSAllowsArbitraryLoads 42 | 43 | 44 | UISupportedInterfaceOrientations~ipad 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationPortraitUpsideDown 48 | UIInterfaceOrientationLandscapeLeft 49 | UIInterfaceOrientationLandscapeRight 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /Example/Example/MySoldiers/ServerEnvironmentSoldier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServerEnvironmentSoldier.swift 3 | // Example 4 | // 5 | // Created by jiaxin on 2019/8/23. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import JXCaptain 11 | 12 | class ServerEnvironmentSoldier: Soldier { 13 | public var name: String 14 | public var team: String 15 | public var icon: UIImage? 16 | public var contentView: UIView? 17 | public var hasNewEvent: Bool = false 18 | 19 | public init() { 20 | name = "线上环境" 21 | team = "业务工具" 22 | let myContentView = ServerEnvironmentContentView() 23 | myContentView.toggle.isOn = true 24 | myContentView.nameLabel.text = name 25 | contentView = myContentView 26 | } 27 | 28 | public func prepare() { 29 | } 30 | 31 | public func action(naviController: UINavigationController) { 32 | } 33 | } 34 | 35 | class ServerEnvironmentContentView: UIView { 36 | let toggle: UISwitch 37 | let nameLabel: UILabel 38 | 39 | override init(frame: CGRect) { 40 | toggle = UISwitch() 41 | nameLabel = UILabel() 42 | super.init(frame: frame) 43 | 44 | toggle.isOn = UserDefaults.standard.bool(forKey: "kServerEnvironment") 45 | toggle.translatesAutoresizingMaskIntoConstraints = false 46 | toggle.addTarget(self, action: #selector(toggleDidClick), for: .valueChanged) 47 | addSubview(toggle) 48 | toggle.topAnchor.constraint(equalTo: self.topAnchor, constant: 5).isActive = true 49 | toggle.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true 50 | 51 | nameLabel.textColor = .gray 52 | nameLabel.font = .systemFont(ofSize: 12) 53 | nameLabel.textAlignment = .center 54 | nameLabel.translatesAutoresizingMaskIntoConstraints = false 55 | addSubview(nameLabel) 56 | nameLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true 57 | nameLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true 58 | nameLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true 59 | } 60 | 61 | required init?(coder aDecoder: NSCoder) { 62 | fatalError("init(coder:) has not been implemented") 63 | } 64 | 65 | @objc func toggleDidClick() { 66 | UserDefaults.standard.set(toggle.isOn, forKey: "kServerEnvironment") 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Example/Example/NetworkTestViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkTestViewController.swift 3 | // Example 4 | // 5 | // Created by jiaxin on 2019/8/29. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class NetworkTestViewController: UITableViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | 16 | title = "网络测试" 17 | tableView.tableFooterView = UIView() 18 | } 19 | 20 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 21 | if indexPath.row == 0 { 22 | let url = URL(string: "https://www.taobao.com/")! 23 | let request = URLRequest(url: url) 24 | NSURLConnection.sendAsynchronousRequest(request, queue: OperationQueue()) { (response, data, error) in 25 | if response != nil { 26 | print(response!) 27 | } 28 | } 29 | }else if indexPath.row == 1 { 30 | let url = URL(string: "https://www.jd.com/")! 31 | let request = URLRequest(url: url) 32 | let task = URLSession.shared.dataTask(with: request) { (data, response, error) in 33 | if response != nil { 34 | print(response!) 35 | } 36 | } 37 | task.resume() 38 | }else if indexPath.row == 2 { 39 | let url = URL(string: "https://cn.bing.com/th?id=OIP.MehRXMQpJaV17DMm_MySxgHaFR&pid=Api&rs=1")! 40 | let request = URLRequest(url: url) 41 | let task = URLSession.shared.dataTask(with: request) { (data, response, error) in 42 | if response != nil { 43 | print(response!) 44 | } 45 | } 46 | task.resume() 47 | }else if indexPath.row == 3 { 48 | let url = URL(string: "http://pic38.nipic.com/20140217/14150008_155520585000_2.gif")! 49 | let request = URLRequest(url: url) 50 | let task = URLSession.shared.dataTask(with: request) { (data, response, error) in 51 | if response != nil { 52 | print(response!) 53 | } 54 | } 55 | task.resume() 56 | }else if indexPath.row == 4 { 57 | let url = URL(string: "http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4")! 58 | let request = URLRequest(url: url) 59 | let task = URLSession.shared.dataTask(with: request) { (data, response, error) in 60 | if response != nil { 61 | print(response!) 62 | } 63 | } 64 | task.resume() 65 | }else if indexPath.row == 5 { 66 | let url = URL(string: "https://gitee.com/xiansanyee/codes/saix3etdh0bywm4pr9gzl38/raw?blob_name=News.mp3")! 67 | let request = URLRequest(url: url) 68 | let task = URLSession.shared.dataTask(with: request) { (data, response, error) in 69 | if response != nil { 70 | print(response!) 71 | } 72 | } 73 | task.resume() 74 | }else if indexPath.row == 6 { 75 | let vc = UIWebViewViewController() 76 | navigationController?.pushViewController(vc, animated: true) 77 | } 78 | 79 | if indexPath.row != 6 { 80 | let alert = UIAlertController(title: nil, message: "发送成功", preferredStyle: .alert) 81 | alert.addAction(UIAlertAction(title: "确定", style: .cancel, handler: nil)) 82 | present(alert, animated: true, completion: nil) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Example/Example/PerformanceTestViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PerformanceTestViewController.swift 3 | // Example 4 | // 5 | // Created by jiaxin on 2019/8/28. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class PerformanceTestViewController: UITableViewController { 12 | var texts = [NSAttributedString]() 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | 17 | title = "性能测试" 18 | tableView.tableFooterView = UIView() 19 | } 20 | 21 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 22 | if indexPath.row == 0 { 23 | //卡顿 24 | Thread.sleep(forTimeInterval: 2) 25 | }else if indexPath.row == 1 { 26 | //低fps 27 | Thread.sleep(forTimeInterval: 0.5) 28 | }else if indexPath.row == 2 { 29 | //高cpu 30 | for number in 1..<10000000 { 31 | sqrt(pow(Double(number), Double(number))) 32 | } 33 | }else if indexPath.row == 3 { 34 | //高内存 35 | for index in 0..<100000 { 36 | texts.append(NSAttributedString(string: "\(index)")) 37 | } 38 | }else if indexPath.row == 4 { 39 | //高流量 40 | for _ in 0..<10 { 41 | let url = URL(string: "https://www.taobao.com/")! 42 | let request = URLRequest(url: url) 43 | let task = URLSession.shared.dataTask(with: request) { (data, response, error) in 44 | if response != nil { 45 | print(response!) 46 | } 47 | } 48 | task.resume() 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Example/Example/SanboxTestViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SanboxTestViewController.swift 3 | // Example 4 | // 5 | // Created by jiaxin on 2019/8/23. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SanboxTestViewController: UITableViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | 16 | title = "沙盒测试" 17 | tableView.tableFooterView = UIView() 18 | } 19 | 20 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 21 | guard let documentURL = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) else { 22 | return 23 | } 24 | var toastText = "添加成功" 25 | if indexPath.row == 0 { 26 | let formatter = DateFormatter() 27 | formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" 28 | formatter.timeZone = TimeZone.current 29 | let dateString = formatter.string(from: Date()) 30 | let fileURL = documentURL.appendingPathComponent("\(dateString).txt") 31 | _ = try? dateString.write(to: fileURL, atomically: true, encoding: .utf8) 32 | do { 33 | try dateString.write(to: fileURL, atomically: true, encoding: .utf8) 34 | }catch (let error) { 35 | toastText = error.localizedDescription 36 | } 37 | }else if indexPath.row == 1 { 38 | let image = UIImage(named: "lufei.jpg") 39 | let imageURL = documentURL.appendingPathComponent("lufei.jpg") 40 | let result = FileManager.default.createFile(atPath: imageURL.path, contents: image?.jpegData(compressionQuality: 1), attributes: nil) 41 | if !result { 42 | toastText = "添加失败" 43 | } 44 | }else if indexPath.row == 2 { 45 | let image = UIImage(named: "icon_shield.png") 46 | let imageURL = documentURL.appendingPathComponent("icon_shield.png") 47 | let result = FileManager.default.createFile(atPath: imageURL.path, contents: image?.jpegData(compressionQuality: 1), attributes: nil) 48 | if !result { 49 | toastText = "添加失败" 50 | } 51 | }else if indexPath.row == 3 { 52 | let url = URL(string: "http://pic38.nipic.com/20140217/14150008_155520585000_2.gif")! 53 | let request = URLRequest(url: url) 54 | let semaphore = DispatchSemaphore(value: 0) 55 | let task = URLSession.shared.dataTask(with: request) { (data, response, error) in 56 | if data != nil { 57 | if let documentURL = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) { 58 | let imageURL = documentURL.appendingPathComponent("lufei.gif") 59 | let result = FileManager.default.createFile(atPath: imageURL.path, contents: data, attributes: nil) 60 | if !result { 61 | toastText = "添加失败" 62 | } 63 | } 64 | } 65 | semaphore.signal() 66 | } 67 | task.resume() 68 | let waitResult = semaphore.wait(timeout: .distantFuture) 69 | print(waitResult) 70 | }else if indexPath.row == 4 { 71 | toastText = "正在下载" 72 | let url = URL(string: "http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4")! 73 | let request = URLRequest(url: url) 74 | let task = URLSession.shared.dataTask(with: request) { (data, response, error) in 75 | if data != nil { 76 | if let documentURL = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) { 77 | let imageURL = documentURL.appendingPathComponent("temp.mp4") 78 | let result = FileManager.default.createFile(atPath: imageURL.path, contents: data, attributes: nil) 79 | DispatchQueue.main.async { 80 | if !result { 81 | self.showText("添加视频失败") 82 | }else { 83 | self.showText("添加视频成功") 84 | } 85 | } 86 | } 87 | }else { 88 | DispatchQueue.main.async { 89 | self.showText("添加视频失败") 90 | } 91 | } 92 | } 93 | task.resume() 94 | }else if indexPath.row == 5 { 95 | toastText = "正在下载" 96 | let url = URL(string: "https://gitee.com/xiansanyee/codes/saix3etdh0bywm4pr9gzl38/raw?blob_name=News.mp3")! 97 | let request = URLRequest(url: url) 98 | let task = URLSession.shared.dataTask(with: request) { (data, response, error) in 99 | if data != nil { 100 | if let documentURL = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) { 101 | let imageURL = documentURL.appendingPathComponent("temp.mp3") 102 | let result = FileManager.default.createFile(atPath: imageURL.path, contents: data, attributes: nil) 103 | DispatchQueue.main.async { 104 | if !result { 105 | self.showText("添加音频失败") 106 | }else { 107 | self.showText("添加音频成功") 108 | } 109 | } 110 | } 111 | }else { 112 | DispatchQueue.main.async { 113 | self.showText("添加音频失败") 114 | } 115 | } 116 | } 117 | task.resume() 118 | } 119 | showText(toastText) 120 | } 121 | 122 | func showText(_ text: String) { 123 | if presentedViewController != nil { 124 | dismiss(animated: false) { 125 | let alert = UIAlertController(title: nil, message: text, preferredStyle: .alert) 126 | alert.addAction(UIAlertAction(title: "确定", style: .cancel, handler: nil)) 127 | self.present(alert, animated: true, completion: nil) 128 | } 129 | }else { 130 | let alert = UIAlertController(title: nil, message: text, preferredStyle: .alert) 131 | alert.addAction(UIAlertAction(title: "确定", style: .cancel, handler: nil)) 132 | self.present(alert, animated: true, completion: nil) 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Example/Example/UIWebViewViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIWebViewViewController.swift 3 | // Example 4 | // 5 | // Created by jiaxin on 2019/9/6. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class UIWebViewViewController: UIViewController { 12 | var webView: UIWebView! 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | 17 | webView = UIWebView(frame: CGRect.zero) 18 | view.addSubview(webView) 19 | 20 | let url = URL(string: "https://www.baidu.com/")! 21 | let request = URLRequest(url: url) 22 | webView.loadRequest(request) 23 | } 24 | 25 | override func viewDidLayoutSubviews() { 26 | super.viewDidLayoutSubviews() 27 | 28 | webView.frame = view.bounds 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Example/Example/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Example 4 | // 5 | // Created by jiaxin on 2019/8/20. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import JXCaptain 11 | 12 | class ViewController: UITableViewController { 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | 17 | navigationItem.rightBarButtonItem = UIBarButtonItem(title: "呼叫队长", style: .plain, target: self, action: #selector(naviRightItemDidClick)) 18 | tableView.tableFooterView = UIView() 19 | 20 | //在prepare之前添加自定义Soldier 21 | Captain.default.enqueueSoldiers([ServerEnvironmentSoldier()]) 22 | //在prepare之前赋值自定义Soldier闭包 23 | Captain.default.configSoldierClosure = { (soldier) in 24 | /* 25 | if let websiteEntry = soldier as? WebsiteEntrySoldier { 26 | //设置H5任意门默认网址 27 | websiteEntry.defaultWebsite = "https://www.baidu.com" 28 | //设置H5任意门自定义落地页面,比如项目有自定义WKWebView、有JS交互逻辑等 29 | websiteEntry.webDetailControllerClosure = { (website) in 30 | return UIViewController() 31 | } 32 | } 33 | */ 34 | /* 35 | if let anr = soldier as? ANRSoldier { 36 | //设置卡顿时间阈值,单位秒 37 | anr.threshold = 2 38 | } 39 | */ 40 | /* 41 | if let crash = soldier as? CrashSoldier { 42 | //自定义处理crash信息,可以存储到本地,下次打开app再传送到服务器记录 43 | crash.exceptionReceiveClosure = { (signal, exception, info) in 44 | if signal != nil { 45 | print("signal crash info:\(info ?? "")") 46 | }else if exception != nil { 47 | print("exception crash info:\(info ?? "")") 48 | } 49 | } 50 | } 51 | */ 52 | } 53 | 54 | //最后再调用prepare 55 | Captain.default.prepare() 56 | } 57 | 58 | @objc func naviRightItemDidClick() { 59 | Captain.default.show() 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /Example/Example/lufei.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pujiaxin33/JXCaptain/099dfe2118f798ac3a355521203980569e2d8e99/Example/Example/lufei.jpg -------------------------------------------------------------------------------- /JXCaptain.podspec: -------------------------------------------------------------------------------- 1 | 2 | Pod::Spec.new do |s| 3 | s.name = "JXCaptain" 4 | s.version = "0.0.13" 5 | s.summary = "一个应用调试工具箱" 6 | s.homepage = "https://github.com/pujiaxin33/JXCaptain" 7 | s.license = "MIT" 8 | s.author = { "pujiaxin33" => "317437084@qq.com" } 9 | s.platform = :ios, "9.0" 10 | s.swift_version = "5.0" 11 | s.source = { :git => "https://github.com/pujiaxin33/JXCaptain.git", :tag => "#{s.version}" } 12 | s.framework = "UIKit" 13 | s.source_files = "Sources", "Sources/**/*.{swift}" 14 | s.resource = 'Resources/Resource.bundle' 15 | s.requires_arc = true 16 | 17 | s.dependency 'BSBacktraceLogger' 18 | end 19 | -------------------------------------------------------------------------------- /JXCaptain.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /JXCaptain.xcodeproj/xcuserdata/jiaxin.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | JXCaptain.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 2 11 | 12 | ResourceBundle.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 2 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /JXCaptain/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /JXCaptain/JXCaptain.h: -------------------------------------------------------------------------------- 1 | // 2 | // JXCaptain.h 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/20. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for JXCaptain. 12 | FOUNDATION_EXPORT double JXCaptainVersionNumber; 13 | 14 | //! Project version string for JXCaptain. 15 | FOUNDATION_EXPORT const unsigned char JXCaptainVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 暴走的鑫鑫 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://github.com/pujiaxin33/JXExampleImages/blob/master/JXCaptain/JXCaptain_icon_shield.png) 2 | # JXCaptain 3 | 4 | 像美国队长一样威猛的应用调试工具箱! 5 | 6 | # 前言 7 | 8 | 每个稍有规模的APP都会有一些开发测试功能,比如切换服务器环境、浏览沙盒文件、查看页面帧率以及接口请求日志等。之前项目内部前前后后实现了一些功能,直到看到了滴滴开源的[DoraemonKit](https://github.com/didi/DoraemonKit)。被它完整的功能实现所折服,引入之后真的就拥有了强大的工具集合。在是否要引入`DoraemonKit`之际,我陷入了深深的思考。第一,项目几乎是纯Swift语言开发,并且尽量不再去引用OC的库。第二,内部已经实现了许多功能,稍加改造也能转正为人名服务。所以,就借鉴了`DoraemonKit`的思路和部分代码,重新实现了`JXCaptain`,一个强大的Swift编写的应用调试工具箱。 9 | 10 | 取名`Captain`队长的含义,就是把一个个功能比喻为战士,队长就是指挥这些战士的精神领袖。有了队长就能所向披靡,战无不胜! 11 | 12 | # 预览 13 | 14 |
15 | 16 | # 功能模块 17 | 18 | ## 常用工具 19 | 20 | - **【App信息】** 快速查看手机信息,App 信息,权限信息的渠道,避免去手机设置查找或者查看项目源代码的麻烦; 21 | - **【沙盒浏览】** 沙盒目录浏览,支持图片、视频、sql数据库等文件预览,支持文件的共享; 22 | - **【Crash日志】** 捕获Crash,并将日志通过文件形式保存在沙盒里面; 23 | - **【H5任意门】** 开始输入H5地址进行网页功能验证; 24 | - **【UserDefauls】** 查看`UserDefauls.shared`记录的键值对数据; 25 | 26 | ## 性能检测 27 | 28 | - **【FPS】** 顶部状态栏实时显示当前的帧率; 29 | - **【内存】** 顶部状态栏实时显示当前的内存占用; 30 | - **【CPU】** 顶部状态栏实时显示当前的CPU使用率; 31 | - **【卡顿】** 当页面出现卡顿是,保存当时的堆栈信息,使用`BSBacktraceLogger`获取堆栈信息; 32 | - **【流量】** 监听APP和网页的接口调用,查看Header、Response等信息; 33 | 34 | 35 | # 自定义工具 36 | 37 | ## 实现`Soldier`协议 38 | 39 | ```Swift 40 | public class UserDefaultsSoldier: Soldier { 41 | public static let shared = UserDefaultsSoldier() 42 | //工具列表显示的名字 43 | public var name: String 44 | //归属于哪个团队?比如:常用工具、性能检测或者自定义团队名 45 | public var team: String 46 | //工具列表显示的icon 47 | public var icon: UIImage? 48 | //如果你不适用默认的name加icon的显示,直接创建新视图赋值给contentView即可 49 | public var contentView: UIView? 50 | //是否监听到了新的事件,比如Crash日志捕获到了新的Crash,将hasNewEvent设置为true,就会显示红点提示 51 | public var hasNewEvent: Bool = false 52 | 53 | public init() { 54 | name = "UserDefaults" 55 | team = "常用工具" 56 | icon = ImageManager.imageWithName("JXCaptain_icon_app_user_defaults") 57 | } 58 | //APP启动之后会调用该方法,用于启动当前工具的工作。比如帧率检测,调用prepare之后就开始检测帧率并显示到顶部状态了。 59 | public func prepare() { } 60 | //用点击了工具列表的Icon之后,就行页面的调整。一般工具都会有一个进行配置的Dashboard页面 61 | public func action(naviController: UINavigationController) { 62 | naviController.pushViewController(UserDefaultsKeyValuesListViewController(defaults: UserDefaults.standard), animated: true) 63 | } 64 | } 65 | ``` 66 | 67 | ## 添加新的`Soldier` 68 | 69 | ```Swift 70 | Captain.default.enqueueSoldiers([ServerEnvironmentSoldier()]) 71 | ``` 72 | 73 | ## 配置已有的`Soldier` 74 | 75 | ```Swift 76 | Captain.default.configSoldierClosure = { (soldier) in 77 | if let websiteEntry = soldier as? WebsiteEntrySoldier { 78 | //设置H5任意门默认网址 79 | websiteEntry.defaultWebsite = "https://www.baidu.com" 80 | //设置H5任意门自定义落地页面,比如项目有自定义WKWebView、有JS交互逻辑等 81 | websiteEntry.webDetailControllerClosure = { (website) in 82 | return UIViewController() 83 | } 84 | } 85 | if let anr = soldier as? ANRSoldier { 86 | //设置卡顿时间阈值,单位秒 87 | anr.threshold = 2 88 | } 89 | if let crash = soldier as? CrashSoldier { 90 | //自定义处理crash信息,可以存储到本地,下次打开app再传送到服务器记录 91 | crash.exceptionReceiveClosure = { (signal, exception, info) in 92 | if signal != nil { 93 | print("signal crash info:\(info ?? "")") 94 | }else if exception != nil { 95 | print("exception crash info:\(info ?? "")") 96 | } 97 | } 98 | } 99 | } 100 | ``` 101 | 102 | ## 启动工具 103 | 104 | ```Swift 105 | Captain.default.prepare() 106 | ``` 107 | 108 | # `Captain`的显示与隐藏 109 | 110 | 可以监听摇一摇、或者在Window上面添加两指连击4次,就显示`Captain`。调用如下代码即可: 111 | ```Swift 112 | Captain.default.show() 113 | ``` 114 | 115 | 隐藏`Captain`调用如下代码: 116 | ```Swift 117 | Captain.default.hide() 118 | ``` 119 | 120 | 121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /Resources/Resource.bundle/JXCaptain_icon_anr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pujiaxin33/JXCaptain/099dfe2118f798ac3a355521203980569e2d8e99/Resources/Resource.bundle/JXCaptain_icon_anr.png -------------------------------------------------------------------------------- /Resources/Resource.bundle/JXCaptain_icon_app_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pujiaxin33/JXCaptain/099dfe2118f798ac3a355521203980569e2d8e99/Resources/Resource.bundle/JXCaptain_icon_app_info.png -------------------------------------------------------------------------------- /Resources/Resource.bundle/JXCaptain_icon_app_user_defaults.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pujiaxin33/JXCaptain/099dfe2118f798ac3a355521203980569e2d8e99/Resources/Resource.bundle/JXCaptain_icon_app_user_defaults.png -------------------------------------------------------------------------------- /Resources/Resource.bundle/JXCaptain_icon_cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pujiaxin33/JXCaptain/099dfe2118f798ac3a355521203980569e2d8e99/Resources/Resource.bundle/JXCaptain_icon_cpu.png -------------------------------------------------------------------------------- /Resources/Resource.bundle/JXCaptain_icon_crash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pujiaxin33/JXCaptain/099dfe2118f798ac3a355521203980569e2d8e99/Resources/Resource.bundle/JXCaptain_icon_crash.png -------------------------------------------------------------------------------- /Resources/Resource.bundle/JXCaptain_icon_fps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pujiaxin33/JXCaptain/099dfe2118f798ac3a355521203980569e2d8e99/Resources/Resource.bundle/JXCaptain_icon_fps.png -------------------------------------------------------------------------------- /Resources/Resource.bundle/JXCaptain_icon_h5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pujiaxin33/JXCaptain/099dfe2118f798ac3a355521203980569e2d8e99/Resources/Resource.bundle/JXCaptain_icon_h5.png -------------------------------------------------------------------------------- /Resources/Resource.bundle/JXCaptain_icon_memory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pujiaxin33/JXCaptain/099dfe2118f798ac3a355521203980569e2d8e99/Resources/Resource.bundle/JXCaptain_icon_memory.png -------------------------------------------------------------------------------- /Resources/Resource.bundle/JXCaptain_icon_network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pujiaxin33/JXCaptain/099dfe2118f798ac3a355521203980569e2d8e99/Resources/Resource.bundle/JXCaptain_icon_network.png -------------------------------------------------------------------------------- /Resources/Resource.bundle/JXCaptain_icon_sanbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pujiaxin33/JXCaptain/099dfe2118f798ac3a355521203980569e2d8e99/Resources/Resource.bundle/JXCaptain_icon_sanbox.png -------------------------------------------------------------------------------- /Resources/Resource.bundle/JXCaptain_icon_shield.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pujiaxin33/JXCaptain/099dfe2118f798ac3a355521203980569e2d8e99/Resources/Resource.bundle/JXCaptain_icon_shield.png -------------------------------------------------------------------------------- /Sources/Captain.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Captain.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/20. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class Captain { 12 | public static let `default` = Captain() 13 | public var configSoldierClosure: ((Soldier)->())? 14 | public var screenEdgeInsets: UIEdgeInsets 15 | public var logoImage: UIImage 16 | internal var soldiers = [Soldier]() 17 | internal let floatingWindow = CaptainFloatingWindow(frame: CGRect.zero) 18 | 19 | init() { 20 | let defaultSoldiers: [Soldier] = [AppInfoSoldier(), SanboxBrowserSoldier(), CrashSoldier(), WebsiteEntrySoldier(), FPSSoldier(), MemorySoldier(), CPUSoldier(), ANRSoldier(), NetworkObserverSoldier.shared, UserDefaultsSoldier()] 21 | soldiers.append(contentsOf: defaultSoldiers) 22 | var topEdgeInset: CGFloat = 20 23 | var bottomEdgeInset: CGFloat = 12 24 | if #available(iOS 11.0, *) { 25 | let safeAreaInsets = floatingWindow.safeAreaInsets 26 | if safeAreaInsets.top > 0 { 27 | topEdgeInset = safeAreaInsets.top 28 | } 29 | if safeAreaInsets.bottom > 0 { 30 | bottomEdgeInset = safeAreaInsets.bottom 31 | } 32 | } 33 | screenEdgeInsets = UIEdgeInsets(top: topEdgeInset, left: 12, bottom: bottomEdgeInset, right: 12) 34 | logoImage = ImageManager.imageWithName("JXCaptain_icon_shield")! 35 | } 36 | 37 | public func show() { 38 | floatingWindow.isHidden = false 39 | } 40 | 41 | public func hide() { 42 | floatingWindow.rootViewController?.dismiss(animated: false, completion: nil) 43 | floatingWindow.isHidden = true 44 | } 45 | 46 | public func prepare() { 47 | soldiers.forEach { configSoldierClosure?($0) } 48 | soldiers.forEach { $0.prepare() } 49 | } 50 | 51 | public func enqueueSoldiers(_ soldiers: [Soldier]) { 52 | self.soldiers.append(contentsOf: soldiers) 53 | } 54 | 55 | public func removeAllSoldiers() { 56 | soldiers.removeAll() 57 | } 58 | 59 | public func dequeueSoldiers(soldierTypes: [Soldier.Type]) { 60 | for (index, soldier) in soldiers.enumerated() { 61 | let currentSoldierType = type(of: soldier) 62 | let isExisted = soldierTypes.contains { (type) -> Bool in 63 | if type == currentSoldierType { 64 | return true 65 | }else { 66 | return false 67 | } 68 | } 69 | if isExisted { 70 | soldiers.remove(at: index) 71 | break 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/CaptainDefines.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CaptainDefines.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/26. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol Soldier { 12 | var name: String { get } 13 | var team: String { get } 14 | var icon: UIImage? { get } 15 | var contentView: UIView? { get } 16 | var hasNewEvent: Bool { get } 17 | func prepare() 18 | func willAppear() 19 | func action(naviController: UINavigationController) 20 | } 21 | 22 | //hasNewEvent、contentView添加默认实现,有需要的Soldier才添加。 23 | public extension Soldier { 24 | var hasNewEvent: Bool { return false } 25 | var contentView: UIView? { return nil } 26 | func willAppear() { } 27 | } 28 | 29 | protocol Monitor { 30 | associatedtype ValueType 31 | var valueDidUpdateClosure: ((ValueType) -> Void)? { set get } 32 | func start() 33 | func end() 34 | } 35 | 36 | enum MonitorType { 37 | case fps 38 | case memory 39 | case cpu 40 | case anr 41 | } 42 | 43 | public extension Notification.Name { 44 | static let JXCaptainSoldierNewEventDidChange = Notification.Name("JXCaptainSoldierNewEventDidChange") 45 | static let JXCaptainNetworkObserverSoldierNewFlowDidReceive = Notification.Name("JXCaptainNetworkObserverSoldierNewFlowDidReceive") 46 | } 47 | -------------------------------------------------------------------------------- /Sources/Manager/File/ANRFileManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ANRFileManager.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/28. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class ANRFileManager: SoldierFileManager { 12 | public static func directoryURL() -> URL? { 13 | let fileManager = FileManager.default 14 | guard let cacheURL = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first else { 15 | return nil 16 | } 17 | let tempDirectoryURL = cacheURL.appendingPathComponent("com.JXCaptain.anr", isDirectory: true) 18 | if !fileManager.fileExists(atPath: tempDirectoryURL.path) { 19 | do { 20 | try fileManager.createDirectory(at: tempDirectoryURL, withIntermediateDirectories: true, attributes: nil) 21 | }catch (let error){ 22 | print("ANRFileManager create ANR directory error:\(error.localizedDescription)") 23 | return nil 24 | } 25 | } 26 | return tempDirectoryURL 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Manager/File/CrashFileManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CrashFileManager.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/21. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class CrashFileManager: SoldierFileManager { 12 | public static func directoryURL() -> URL? { 13 | let fileManager = FileManager.default 14 | guard let cacheURL = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first else { 15 | return nil 16 | } 17 | let directoryURL = cacheURL.appendingPathComponent("com.JXCaptain.crash", isDirectory: true) 18 | if !fileManager.fileExists(atPath: directoryURL.path) { 19 | do { 20 | try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) 21 | }catch (let error){ 22 | print("CrashFileManager create crash directory error:\(error.localizedDescription)") 23 | return nil 24 | } 25 | } 26 | return directoryURL 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Manager/File/SoldierFileManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SoldierFileManager.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/29. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol SoldierFileManager { 12 | static func saveInfo(_ info: String, fileName: String?, fileNamePrefix: String?) 13 | static func directoryURL() -> URL? 14 | static func allFiles() -> [SanboxModel] 15 | static func deleteAllFiles() 16 | } 17 | 18 | public extension SoldierFileManager { 19 | static func saveInfo(_ info: String, fileName: String? = nil, fileNamePrefix: String? = nil) { 20 | guard !info.isEmpty else { 21 | return 22 | } 23 | var targetFileName: String? 24 | if fileName != nil { 25 | targetFileName = fileName 26 | }else { 27 | let dateFormatter = DateFormatter() 28 | dateFormatter.timeZone = TimeZone.current 29 | dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" 30 | let dateString = dateFormatter.string(from: Date()) 31 | if fileNamePrefix != nil { 32 | targetFileName = "\(fileNamePrefix!)-\(dateString)" 33 | }else { 34 | targetFileName = dateString 35 | } 36 | } 37 | 38 | guard let fileURL = directoryURL()?.appendingPathComponent("\(targetFileName!).txt") else { 39 | return 40 | } 41 | do { 42 | try info.write(to: fileURL, atomically: true, encoding: .utf8) 43 | } catch(let error) { 44 | print("write info error:\(error.localizedDescription)") 45 | } 46 | } 47 | 48 | static func directoryURL() -> URL? { 49 | return nil 50 | } 51 | 52 | static func allFiles() -> [SanboxModel] { 53 | guard let targetDirectory = directoryURL() else { 54 | return [SanboxModel]() 55 | } 56 | let fileManager = FileManager.default 57 | guard let fileURLs = try? fileManager.contentsOfDirectory(at: targetDirectory, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) else { 58 | return [SanboxModel]() 59 | } 60 | let sortedFileURLs = fileURLs.sorted { (first, second) -> Bool in 61 | let firstAttributes = try? fileManager.attributesOfItem(atPath: first.path) 62 | let secondAttributes = try? fileManager.attributesOfItem(atPath: second.path) 63 | let firstDate = firstAttributes?[FileAttributeKey.creationDate] as? Date 64 | let secondDate = secondAttributes?[FileAttributeKey.creationDate] as? Date 65 | if firstDate != nil && secondDate != nil { 66 | return firstDate! > secondDate! 67 | } 68 | return false 69 | } 70 | var sanboxInfos = [SanboxModel]() 71 | for fileURL in sortedFileURLs { 72 | let model = SanboxModel(fileURL: fileURL, name: fileURL.lastPathComponent) 73 | sanboxInfos.append(model) 74 | } 75 | return sanboxInfos 76 | } 77 | 78 | static func deleteAllFiles() { 79 | guard let targetDirectory = directoryURL() else { 80 | return 81 | } 82 | let fileManager = FileManager.default 83 | do { 84 | try fileManager.removeItem(at: targetDirectory) 85 | }catch (let error) { 86 | print("deleteAllFiles error:\(error.localizedDescription)") 87 | } 88 | } 89 | } 90 | 91 | -------------------------------------------------------------------------------- /Sources/Manager/Image/ImageManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageManager.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/22. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class ImageManager { 12 | static func imageWithName(_ name: String) -> UIImage? { 13 | let frameworkBundle = Bundle(for: Captain.self) 14 | let resourceBundleURL = frameworkBundle.url(forResource: "Resource", withExtension: "bundle") 15 | let resourceBundle = Bundle(url: resourceBundleURL!) 16 | return UIImage(named: name, in: resourceBundle, compatibleWith: nil) 17 | } 18 | } 19 | 20 | 21 | -------------------------------------------------------------------------------- /Sources/Manager/NetworkManager/NetworkManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkManager.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/29. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class NetworkManager { 12 | static func httpBody(request: URLRequest) -> Data? { 13 | if request.httpBody != nil { 14 | return request.httpBody 15 | }else if request.httpMethod == "POST" { 16 | let buffer = UnsafeMutablePointer.allocate(capacity: 1024) 17 | let stream = request.httpBodyStream 18 | stream?.open() 19 | var data = Data() 20 | while stream?.hasBytesAvailable == true { 21 | let len = stream?.read(buffer, maxLength: 1024) ?? 0 22 | if len > 0 && stream?.streamError == nil { 23 | data.append(buffer, count: len) 24 | } 25 | } 26 | stream?.close() 27 | buffer.deallocate() 28 | return data 29 | } 30 | return nil 31 | } 32 | 33 | static func jsonString(from data: Data) -> String? { 34 | if data.isEmpty { 35 | return nil 36 | } 37 | guard let object = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) else { 38 | return String(data: data, encoding: .utf8) 39 | } 40 | if JSONSerialization.isValidJSONObject(object) { 41 | if let jsonData = try? JSONSerialization.data(withJSONObject: object, options: .prettyPrinted) { 42 | let string = String(data: jsonData, encoding: .utf8) 43 | return string?.replacingOccurrences(of: "\\/", with: "/") 44 | } 45 | } 46 | return nil 47 | } 48 | 49 | static func flowLengthString(_ length: Int) -> String { 50 | if length < 1024 { 51 | return String(format: "%dBytes", length) 52 | }else if length < 1024 * 1024 { 53 | return String(format: "%.1fKB", Double(length)/1204) 54 | }else { 55 | return String(format: "%.1fMB", Double(length)/1204/1024) 56 | } 57 | } 58 | 59 | static func requestFlowLength(_ request: URLRequest) -> Int { 60 | var headerFields = [String : String]() 61 | if request.allHTTPHeaderFields != nil { 62 | headerFields.merge(request.allHTTPHeaderFields!) { (current, _) -> String in 63 | return current 64 | } 65 | } 66 | if let url = request.url { 67 | let cookies = HTTPCookieStorage.shared.cookies(for: url) 68 | if cookies?.isEmpty == false { 69 | let cookiesHeader = HTTPCookie.requestHeaderFields(with: cookies!) 70 | headerFields.merge(cookiesHeader) { (current, _) -> String in 71 | return current 72 | } 73 | } 74 | } 75 | let headersLength = headerFieldsLength(headerFields) 76 | let bodyLength = httpBody(request: request)?.count ?? 0 77 | return headersLength + bodyLength 78 | } 79 | 80 | static func responseFlowLength(_ response: URLResponse, responseData: Data) -> Int { 81 | return responseData.count 82 | //FIXME: https://stackoverflow.com/questions/39256489/how-to-bridge-a-long-long-c-macro-to-swift 83 | /* 84 | guard let httpResponse = response as? HTTPURLResponse else { 85 | return responseData.count 86 | } 87 | let headersLength = headerFieldsLength(httpResponse.allHeaderFields) 88 | var contentLength: Int = 0 89 | if httpResponse.expectedContentLength != NSURLResponseUnknownLength { 90 | contentLength = httpResponse.expectedContentLength 91 | }else { 92 | contentLength = responseData.count 93 | } 94 | return headersLength + contentLength 95 | */ 96 | } 97 | 98 | static func responseData(requestID: String) -> Data? { 99 | return NetworkObserverSoldier.shared.cache.object(forKey: requestID as AnyObject) as? Data 100 | } 101 | 102 | static func responseImage(requestID: String) -> UIImage? { 103 | let responseData = NetworkObserverSoldier.shared.cache.object(forKey: requestID as AnyObject) as? Data 104 | if responseData != nil { 105 | return UIImage(data: responseData!, scale: UIScreen.main.scale) 106 | } 107 | return nil 108 | } 109 | 110 | static func responseImages(requestID: String) -> [UIImage]? { 111 | let responseData = NetworkObserverSoldier.shared.cache.object(forKey: requestID as AnyObject) as? Data 112 | if responseData != nil, let imageSource = CGImageSourceCreateWithData(responseData! as CFData, nil) { 113 | let imagesCount = CGImageSourceGetCount(imageSource) 114 | var images = [UIImage]() 115 | for index in 0.. String? { 126 | let responseData = NetworkObserverSoldier.shared.cache.object(forKey: requestID as AnyObject) as? Data 127 | if responseData != nil { 128 | return NetworkManager.jsonString(from: responseData!) 129 | } 130 | return nil 131 | } 132 | 133 | //MARK: - Private 134 | 135 | private static func headerFieldsLength(_ headerFields: Any) -> Int { 136 | let data = try? JSONSerialization.data(withJSONObject: headerFields, options: .prettyPrinted) 137 | return data?.count ?? 0 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Sources/Soldiers/ANR/ANRMonitor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ANRMonitor.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/28. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class ANRMonitor: Monitor { 12 | typealias ValueType = Double 13 | var valueDidUpdateClosure: ((ValueType) -> Void)? 14 | var threshold: Double = 1 15 | private var thread: ANRPingThread? 16 | 17 | deinit { 18 | valueDidUpdateClosure = nil 19 | end() 20 | } 21 | 22 | func start() { 23 | end() 24 | thread = ANRPingThread(threshold: threshold, handler: {[weak self] (value) in 25 | self?.valueDidUpdateClosure?(value) 26 | }) 27 | thread?.start() 28 | } 29 | 30 | func end() { 31 | thread?.cancel() 32 | thread = nil 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/Soldiers/ANR/ANRPingThread.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ANRPingThread.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/28. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class ANRPingThread: Thread { 12 | let threshold: Double 13 | let handler: ((Double)->())? 14 | private var isApplicationInActive = true 15 | private let semaphore: DispatchSemaphore 16 | private var isMainThreadBlocked: Bool = false 17 | 18 | public init(threshold: Double, handler: ((Double)->())?) { 19 | self.threshold = threshold 20 | self.handler = handler 21 | semaphore = DispatchSemaphore(value: 0) 22 | super.init() 23 | 24 | NotificationCenter.default.addObserver(self, selector: #selector(applicationDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil) 25 | NotificationCenter.default.addObserver(self, selector: #selector(applicationDidEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil) 26 | } 27 | 28 | override public func main() { 29 | while !isCancelled { 30 | if isApplicationInActive { 31 | isMainThreadBlocked = true 32 | DispatchQueue.main.async { 33 | self.isMainThreadBlocked = false 34 | self.semaphore.signal() 35 | } 36 | Thread.sleep(forTimeInterval: self.threshold) 37 | if self.isMainThreadBlocked { 38 | handler?(self.threshold) 39 | } 40 | _ = semaphore.wait(timeout: .distantFuture) 41 | }else { 42 | Thread.sleep(forTimeInterval: self.threshold) 43 | } 44 | } 45 | } 46 | 47 | //MARK: - Private 48 | @objc private func applicationDidBecomeActive() { 49 | isApplicationInActive = true 50 | } 51 | 52 | @objc private func applicationDidEnterBackground() { 53 | isApplicationInActive = false 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/Soldiers/ANR/ANRSoldier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ANRSoldier.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/28. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import BSBacktraceLogger 11 | 12 | public class ANRSoldier: Soldier { 13 | public var name: String 14 | public var team: String 15 | public var icon: UIImage? 16 | public var hasNewEvent: Bool { 17 | set { 18 | if canCheckNewEvent { 19 | UserDefaults.standard.isANRSoldierHasNewEvent = newValue 20 | } 21 | } 22 | get { 23 | if canCheckNewEvent { 24 | return UserDefaults.standard.isANRSoldierHasNewEvent 25 | }else { 26 | return false 27 | } 28 | } 29 | } 30 | public var canCheckNewEvent: Bool = true 31 | public var threshold: Double = 1 32 | var isActive: Bool { 33 | set { UserDefaults.standard.isANRSoldierActive = newValue } 34 | get { UserDefaults.standard.isANRSoldierActive } 35 | } 36 | var monitorView: MonitorConsoleLabel? 37 | let monitor: ANRMonitor 38 | 39 | public init() { 40 | name = "卡顿" 41 | team = "性能检测" 42 | icon = ImageManager.imageWithName("JXCaptain_icon_anr") 43 | monitor = ANRMonitor() 44 | } 45 | 46 | public func prepare() { 47 | if isActive { 48 | start() 49 | } 50 | } 51 | 52 | public func action(naviController: UINavigationController) { 53 | naviController.pushViewController(ANRDashboardViewController(soldier: self), animated: true) 54 | } 55 | 56 | public func start() { 57 | monitor.threshold = threshold 58 | monitor.start() 59 | monitor.valueDidUpdateClosure = {[weak self] (value) in 60 | self?.dump() 61 | } 62 | isActive = true 63 | } 64 | 65 | public func end() { 66 | monitor.end() 67 | isActive = false 68 | } 69 | 70 | func dump() { 71 | guard let threadInfo = BSBacktraceLogger.bs_backtraceOfMainThread() else { 72 | return 73 | } 74 | hasNewEvent = true 75 | ANRFileManager.saveInfo(threadInfo) 76 | NotificationCenter.default.post(name: .JXCaptainSoldierNewEventDidChange, object: self) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/Soldiers/AppInfo/AppInfoSoldier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppInfoSoldier.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/22. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class AppInfoSoldier: Soldier { 12 | public var name: String 13 | public var team: String 14 | public var icon: UIImage? 15 | 16 | public init() { 17 | name = "APP信息" 18 | team = "常用工具" 19 | icon = ImageManager.imageWithName("JXCaptain_icon_app_info") 20 | } 21 | 22 | public func prepare() { 23 | } 24 | 25 | public func action(naviController: UINavigationController) { 26 | naviController.pushViewController(AppInfoViewController(style: .grouped), animated: true) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Soldiers/CPU/CPUMonitor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CPUMonitor.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/28. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MachO 11 | 12 | class CPUMonitor: Monitor { 13 | typealias ValueType = Double 14 | var valueDidUpdateClosure: ((ValueType) -> Void)? 15 | private var timer: Timer? 16 | 17 | deinit { 18 | timer?.invalidate() 19 | timer = nil 20 | valueDidUpdateClosure = nil 21 | } 22 | 23 | func start() { 24 | end() 25 | timer = Timer(timeInterval: 0.5, target: self, selector: #selector(processTimer), userInfo: nil, repeats: true) 26 | timer?.fire() 27 | RunLoop.current.add(timer!, forMode: .common) 28 | } 29 | 30 | func end() { 31 | timer?.invalidate() 32 | timer = nil 33 | } 34 | 35 | @objc func processTimer() { 36 | valueDidUpdateClosure?(cpuUsage()) 37 | } 38 | 39 | private func cpuUsage() -> Double { 40 | var kr: kern_return_t 41 | var task_info_count: mach_msg_type_number_t 42 | task_info_count = mach_msg_type_number_t(TASK_INFO_MAX) 43 | var tinfo = [integer_t](repeating: 0, count: Int(task_info_count)) 44 | 45 | kr = task_info(mach_task_self_, task_flavor_t(TASK_BASIC_INFO), &tinfo, &task_info_count) 46 | if kr != KERN_SUCCESS { 47 | return -1 48 | } 49 | 50 | var thread_list: thread_act_array_t? = UnsafeMutablePointer(mutating: [thread_act_t]()) 51 | 52 | var thread_count: mach_msg_type_number_t = 0 53 | defer { 54 | if thread_list != nil { 55 | vm_deallocate(mach_task_self_, unsafeBitCast(thread_list, to: vm_address_t.self), vm_size_t(Int(thread_count) * MemoryLayout.stride) ) 56 | } 57 | } 58 | kr = task_threads(mach_task_self_, &thread_list, &thread_count) 59 | if kr != KERN_SUCCESS { 60 | return -1 61 | } 62 | 63 | var tot_cpu: Double = 0 64 | 65 | if let thread_list = thread_list { 66 | for j in 0 ..< Int(thread_count) { 67 | var thread_info_count = mach_msg_type_number_t(THREAD_INFO_MAX) 68 | var thinfo = [integer_t](repeating: 0, count: Int(thread_info_count)) 69 | kr = thread_info(thread_list[j], thread_flavor_t(THREAD_BASIC_INFO), 70 | &thinfo, &thread_info_count) 71 | if kr != KERN_SUCCESS { 72 | return -1 73 | } 74 | let threadBasicInfo = convertThreadInfoToThreadBasicInfo(thinfo) 75 | if threadBasicInfo.flags != TH_FLAGS_IDLE { 76 | tot_cpu += (Double(threadBasicInfo.cpu_usage) / Double(TH_USAGE_SCALE)) * 100.0 77 | } 78 | } // for each thread 79 | } 80 | 81 | return tot_cpu 82 | } 83 | 84 | fileprivate func convertThreadInfoToThreadBasicInfo(_ threadInfo: [integer_t]) -> thread_basic_info { 85 | var result = thread_basic_info() 86 | result.user_time = time_value_t(seconds: threadInfo[0], microseconds: threadInfo[1]) 87 | result.system_time = time_value_t(seconds: threadInfo[2], microseconds: threadInfo[3]) 88 | result.cpu_usage = threadInfo[4] 89 | result.policy = threadInfo[5] 90 | result.run_state = threadInfo[6] 91 | result.flags = threadInfo[7] 92 | result.suspend_count = threadInfo[8] 93 | result.sleep_time = threadInfo[9] 94 | return result 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Sources/Soldiers/CPU/CPUSoldier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CPUSoldier.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/26. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class CPUSoldier: Soldier { 12 | public var name: String 13 | public var team: String 14 | public var icon: UIImage? 15 | var isActive: Bool { 16 | set { UserDefaults.standard.isCPUSoldierActive = newValue } 17 | get { UserDefaults.standard.isCPUSoldierActive } 18 | } 19 | var monitorView: MonitorConsoleLabel? 20 | let monitor: CPUMonitor 21 | 22 | public init() { 23 | name = "CPU" 24 | team = "性能检测" 25 | icon = ImageManager.imageWithName("JXCaptain_icon_cpu") 26 | monitor = CPUMonitor() 27 | } 28 | 29 | public func prepare() { 30 | if isActive { 31 | start() 32 | } 33 | } 34 | 35 | public func action(naviController: UINavigationController) { 36 | naviController.pushViewController(CPUDashboardViewController(soldier: self), animated: true) 37 | } 38 | 39 | public func start() { 40 | monitor.start() 41 | monitorView = MonitorConsoleLabel() 42 | monitor.valueDidUpdateClosure = {[weak self] (value) in 43 | self?.monitorView?.update(type: .cpu, value: value) 44 | } 45 | MonitorListWindow.shared.enqueue(monitorView: monitorView!) 46 | isActive = true 47 | } 48 | 49 | public func end() { 50 | monitor.end() 51 | MonitorListWindow.shared.dequeue(monitorView: monitorView!) 52 | isActive = false 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/Soldiers/Crash/CrashSignalExceptionHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CrashSignalExceptionSoldier.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/21. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | typealias SignalHandler = (Int32, UnsafeMutablePointer<__siginfo>?, UnsafeMutableRawPointer?) -> Void 12 | private var previousABRTSignalHandler : SignalHandler? 13 | private var previousBUSSignalHandler : SignalHandler? 14 | private var previousFPESignalHandler : SignalHandler? 15 | private var previousILLSignalHandler : SignalHandler? 16 | private var previousPIPESignalHandler : SignalHandler? 17 | private var previousSEGVSignalHandler : SignalHandler? 18 | private var previousSYSSignalHandler : SignalHandler? 19 | private var previousTRAPSignalHandler : SignalHandler? 20 | private let preHandlers = [SIGABRT : previousABRTSignalHandler, 21 | SIGBUS : previousBUSSignalHandler, 22 | SIGFPE : previousFPESignalHandler, 23 | SIGILL : previousILLSignalHandler, 24 | SIGPIPE : previousPIPESignalHandler, 25 | SIGSEGV : previousSEGVSignalHandler, 26 | SIGSYS : previousSYSSignalHandler, 27 | SIGTRAP : previousTRAPSignalHandler] 28 | 29 | public class CrashSignalExceptionHandler { 30 | public static var exceptionReceiveClosure: ((Int32?, NSException?, String?)->())? 31 | 32 | public func prepare() { 33 | backupOriginalHandler() 34 | signalNewRegister() 35 | } 36 | 37 | func backupOriginalHandler() { 38 | for (signal, handler) in preHandlers { 39 | var tempHandler = handler 40 | backupSingleHandler(signal: signal, preHandler: &tempHandler) 41 | } 42 | } 43 | 44 | func backupSingleHandler(signal: Int32, preHandler: inout SignalHandler?) { 45 | let empty: UnsafeMutablePointer? = nil 46 | var old_action_abrt = sigaction() 47 | sigaction(signal, empty, &old_action_abrt) 48 | if old_action_abrt.__sigaction_u.__sa_sigaction != nil { 49 | preHandler = old_action_abrt.__sigaction_u.__sa_sigaction 50 | } 51 | } 52 | 53 | func signalNewRegister() { 54 | SoldierSignalRegister(signal: SIGABRT) 55 | SoldierSignalRegister(signal: SIGBUS) 56 | SoldierSignalRegister(signal: SIGFPE) 57 | SoldierSignalRegister(signal: SIGILL) 58 | SoldierSignalRegister(signal: SIGPIPE) 59 | SoldierSignalRegister(signal: SIGSEGV) 60 | SoldierSignalRegister(signal: SIGSYS) 61 | SoldierSignalRegister(signal: SIGTRAP) 62 | } 63 | } 64 | 65 | func SoldierSignalRegister(signal: Int32) { 66 | var action = sigaction() 67 | action.__sigaction_u.__sa_sigaction = SoldierSignalHandler 68 | action.sa_flags = SA_NODEFER | SA_SIGINFO 69 | sigemptyset(&action.sa_mask) 70 | let empty: UnsafeMutablePointer? = nil 71 | sigaction(signal, &action, empty) 72 | } 73 | 74 | func SoldierSignalHandler(signal: Int32, info: UnsafeMutablePointer<__siginfo>?, context: UnsafeMutableRawPointer?) { 75 | var exceptionInfo = "Signal Exception:\n" 76 | exceptionInfo.append("Signal \(SignalName(signal)) was raised.\n") 77 | exceptionInfo.append("threadInfo:\n") 78 | exceptionInfo.append("Call Stack:\n") 79 | let callStackSymbols = Thread.callStackSymbols 80 | for index in 0.. String { 94 | switch signal { 95 | case SIGABRT: return "SIGABRT" 96 | case SIGBUS: return "SIGBUS" 97 | case SIGFPE: return "SIGFPE" 98 | case SIGILL: return "SIGILL" 99 | case SIGPIPE: return "SIGPIPE" 100 | case SIGSEGV: return "SIGSEGV" 101 | case SIGSYS: return "SIGSYS" 102 | case SIGTRAP: return "SIGTRAP" 103 | default: return "None" 104 | } 105 | } 106 | 107 | func ClearSignalRigister() { 108 | signal(SIGSEGV,SIG_DFL); 109 | signal(SIGFPE,SIG_DFL); 110 | signal(SIGBUS,SIG_DFL); 111 | signal(SIGTRAP,SIG_DFL); 112 | signal(SIGABRT,SIG_DFL); 113 | signal(SIGILL,SIG_DFL); 114 | signal(SIGPIPE,SIG_DFL); 115 | signal(SIGSYS,SIG_DFL); 116 | } 117 | 118 | -------------------------------------------------------------------------------- /Sources/Soldiers/Crash/CrashSoldier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CrashSoldier.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/22. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class CrashSoldier: Soldier { 12 | public var name: String 13 | public var team: String 14 | public var icon: UIImage? 15 | public var exceptionReceiveClosure: ((Int32?, NSException?, String?)->())? 16 | public var hasNewEvent: Bool { 17 | set { 18 | if canCheckNewEvent { 19 | UserDefaults.standard.isCrashSoldierHasNewEvent = newValue 20 | } 21 | } 22 | get { 23 | if canCheckNewEvent { 24 | return UserDefaults.standard.isCrashSoldierHasNewEvent 25 | }else { 26 | return false 27 | } 28 | } 29 | } 30 | public var canCheckNewEvent: Bool = true 31 | let uncaughtExceptionHandler: CrashUncaughtExceptionHandler 32 | let signalExceptionHandler: CrashSignalExceptionHandler 33 | 34 | public init() { 35 | name = "Crash日志" 36 | team = "常用工具" 37 | icon = ImageManager.imageWithName("JXCaptain_icon_crash") 38 | uncaughtExceptionHandler = CrashUncaughtExceptionHandler() 39 | signalExceptionHandler = CrashSignalExceptionHandler() 40 | CrashUncaughtExceptionHandler.exceptionReceiveClosure = {[weak self] (signal, exception, info) in 41 | self?.exceptionReceiveClosure?(signal, exception, info) 42 | self?.hasNewEvent = true 43 | DispatchQueue.main.async { 44 | NotificationCenter.default.post(name: .JXCaptainSoldierNewEventDidChange, object: self) 45 | } 46 | } 47 | CrashSignalExceptionHandler.exceptionReceiveClosure = {[weak self] (signal, exception, info) in 48 | self?.exceptionReceiveClosure?(signal, exception, info) 49 | self?.hasNewEvent = true 50 | DispatchQueue.main.async { 51 | NotificationCenter.default.post(name: .JXCaptainSoldierNewEventDidChange, object: self) 52 | } 53 | } 54 | } 55 | 56 | public func prepare() { 57 | uncaughtExceptionHandler.prepare() 58 | signalExceptionHandler.prepare() 59 | } 60 | 61 | public func action(naviController: UINavigationController) { 62 | naviController.pushViewController(CrashDashboardViewController(soldier: self), animated: true) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/Soldiers/Crash/CrashUncaughtExceptionHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CrashSoldier.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/20. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | private var preUncaughtExceptionHandler: NSUncaughtExceptionHandler? 12 | 13 | public class CrashUncaughtExceptionHandler { 14 | public static var exceptionReceiveClosure: ((Int32?, NSException?, String?)->())? 15 | 16 | public func prepare() { 17 | preUncaughtExceptionHandler = NSGetUncaughtExceptionHandler() 18 | NSSetUncaughtExceptionHandler(SoldierUncaughtExceptionHandler) 19 | } 20 | } 21 | 22 | 23 | func SoldierUncaughtExceptionHandler(exception: NSException) -> Void { 24 | let stackArray = exception.callStackSymbols 25 | let reason = exception.reason 26 | let name = exception.name.rawValue 27 | let stackInfo = stackArray.reduce("") { (result, item) -> String in 28 | return result + "\n\(item)" 29 | } 30 | let exceptionInfo = name + "\n" + (reason ?? "no reason") + "\n" + stackInfo 31 | CrashUncaughtExceptionHandler.exceptionReceiveClosure?(nil, exception, exceptionInfo) 32 | CrashFileManager.saveInfo(exceptionInfo, fileNamePrefix: "Uncaught:") 33 | preUncaughtExceptionHandler?(exception) 34 | kill(getpid(), SIGKILL) 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Soldiers/FPS/FPSMonitor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FPSMonitor.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/26. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class FPSMonitor: Monitor { 12 | typealias ValueType = Int 13 | var valueDidUpdateClosure: ((ValueType) -> Void)? 14 | var link: CADisplayLink? 15 | var periodCount: Int = 0 16 | var periodStartTime: CFTimeInterval = 0 17 | 18 | 19 | deinit { 20 | link?.invalidate() 21 | link = nil 22 | valueDidUpdateClosure = nil 23 | } 24 | 25 | func start() { 26 | link = CADisplayLink(target: self, selector: #selector(processLink)) 27 | link?.add(to: RunLoop.main, forMode: .common) 28 | } 29 | 30 | func end() { 31 | link?.invalidate() 32 | link = nil 33 | } 34 | 35 | @objc func processLink() { 36 | if periodStartTime == 0 { 37 | periodStartTime = link!.timestamp 38 | return 39 | } 40 | periodCount += 1 41 | let duration = link!.timestamp - periodStartTime 42 | if duration < 1 { 43 | return 44 | } 45 | let fps = CFTimeInterval(periodCount)/duration 46 | valueDidUpdateClosure?(Int(fps + 0.5)) 47 | periodStartTime = link!.timestamp 48 | periodCount = 0 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Soldiers/FPS/FPSSoldier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FPSSoldier.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/26. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class FPSSoldier: Soldier { 12 | public var name: String 13 | public var team: String 14 | public var icon: UIImage? 15 | var isActive: Bool { 16 | set { UserDefaults.standard.isFPSSoldierActive = newValue } 17 | get { UserDefaults.standard.isFPSSoldierActive } 18 | } 19 | var monitorView: MonitorConsoleLabel? 20 | let monitor: FPSMonitor 21 | 22 | public init() { 23 | name = "FPS" 24 | team = "性能检测" 25 | icon = ImageManager.imageWithName("JXCaptain_icon_fps") 26 | monitor = FPSMonitor() 27 | } 28 | 29 | public func prepare() { 30 | if isActive { 31 | start() 32 | } 33 | } 34 | 35 | public func action(naviController: UINavigationController) { 36 | naviController.pushViewController(FPSDashboardViewController(soldier: self), animated: true) 37 | } 38 | 39 | public func start() { 40 | monitor.start() 41 | monitorView = MonitorConsoleLabel() 42 | monitor.valueDidUpdateClosure = {[weak self] (value) in 43 | self?.monitorView?.update(type: .fps, value: Double(value)) 44 | } 45 | MonitorListWindow.shared.enqueue(monitorView: monitorView!) 46 | isActive = true 47 | } 48 | 49 | public func end() { 50 | monitor.end() 51 | MonitorListWindow.shared.dequeue(monitorView: monitorView!) 52 | isActive = false 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/Soldiers/Memory/MemoryMonitor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MemoryMonitor.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/26. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MachO 11 | 12 | class MemoryMonitor: Monitor { 13 | typealias ValueType = Double 14 | var valueDidUpdateClosure: ((ValueType) -> Void)? 15 | private var timer: Timer? 16 | 17 | deinit { 18 | timer?.invalidate() 19 | timer = nil 20 | valueDidUpdateClosure = nil 21 | } 22 | 23 | func start() { 24 | end() 25 | timer = Timer(timeInterval: 0.5, target: self, selector: #selector(processTimer), userInfo: nil, repeats: true) 26 | timer?.fire() 27 | RunLoop.current.add(timer!, forMode: .common) 28 | } 29 | 30 | func end() { 31 | timer?.invalidate() 32 | timer = nil 33 | } 34 | 35 | @objc func processTimer() { 36 | valueDidUpdateClosure?(memory()) 37 | } 38 | 39 | private func memory() -> Double { 40 | var vmInfo = task_vm_info_data_t() 41 | let TASK_VM_INFO_COUNT = MemoryLayout.stride/MemoryLayout.stride 42 | var count = mach_msg_type_number_t(TASK_VM_INFO_COUNT) 43 | let kerr = withUnsafeMutablePointer(to: &vmInfo) { 44 | $0.withMemoryRebound(to: integer_t.self, capacity: 1) { 45 | task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), $0, &count) 46 | } 47 | } 48 | if kerr == KERN_SUCCESS { 49 | return Double(vmInfo.phys_footprint)/1024/1024 50 | }else { 51 | return -1 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/Soldiers/Memory/MemorySoldier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MemorySoldier.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/26. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class MemorySoldier: Soldier { 12 | public var name: String 13 | public var team: String 14 | public var icon: UIImage? 15 | var isActive: Bool { 16 | set { UserDefaults.standard.isMemorySoldierActive = newValue } 17 | get { UserDefaults.standard.isMemorySoldierActive } 18 | } 19 | var monitorView: MonitorConsoleLabel? 20 | let monitor: MemoryMonitor 21 | 22 | public init() { 23 | name = "内存" 24 | team = "性能检测" 25 | icon = ImageManager.imageWithName("JXCaptain_icon_memory") 26 | monitor = MemoryMonitor() 27 | } 28 | 29 | public func prepare() { 30 | if isActive { 31 | start() 32 | } 33 | } 34 | 35 | public func action(naviController: UINavigationController) { 36 | naviController.pushViewController(MemoryDashboardViewController(soldier: self), animated: true) 37 | } 38 | 39 | public func start() { 40 | monitor.start() 41 | monitorView = MonitorConsoleLabel() 42 | monitor.valueDidUpdateClosure = {[weak self] (value) in 43 | self?.monitorView?.update(type: .memory, value: value) 44 | } 45 | MonitorListWindow.shared.enqueue(monitorView: monitorView!) 46 | isActive = true 47 | } 48 | 49 | public func end() { 50 | monitor.end() 51 | MonitorListWindow.shared.dequeue(monitorView: monitorView!) 52 | isActive = false 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/Soldiers/NetworkObserver/JXCaptainURLProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JXCaptainURLProtocol.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/29. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | private let KJXCaptainURLProtocolIdentifier = "KJXCaptainURLProtocolIdentifier" 12 | 13 | class JXCaptainURLProtocol: URLProtocol, URLSessionDataDelegate { 14 | var session: URLSession! 15 | var sessionTask: URLSessionTask? 16 | var receivedData: Data? 17 | var receivedResponse: URLResponse? 18 | var startDate: Date? 19 | var receivedError: Error? 20 | 21 | override init(request: URLRequest, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) { 22 | super.init(request: request, cachedResponse: cachedResponse, client: client) 23 | session = URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: nil) 24 | } 25 | 26 | override class func canInit(with request: URLRequest) -> Bool { 27 | if URLProtocol.property(forKey: KJXCaptainURLProtocolIdentifier, in: request) != nil { 28 | return false 29 | } 30 | if request.url?.scheme != "http" && request.url?.scheme != "https" { 31 | return false 32 | } 33 | return true 34 | } 35 | 36 | override class func canonicalRequest(for request: URLRequest) -> URLRequest { 37 | guard let mutableRequest = ((request as NSURLRequest).mutableCopy() as? NSMutableURLRequest) else { 38 | return request 39 | } 40 | URLProtocol.setProperty("JXCaptain", forKey: KJXCaptainURLProtocolIdentifier, in: mutableRequest) 41 | return mutableRequest as URLRequest 42 | } 43 | 44 | override func startLoading() { 45 | startDate = Date() 46 | sessionTask = session.dataTask(with: request) 47 | sessionTask?.resume() 48 | } 49 | 50 | override func stopLoading() { 51 | sessionTask?.cancel() 52 | sessionTask = nil 53 | receivedData = nil 54 | receivedResponse = nil 55 | startDate = nil 56 | receivedError = nil 57 | } 58 | 59 | func recordRequest() { 60 | guard let receivedResponse = receivedResponse, let receivedData = receivedData, let startDate = startDate else { 61 | return 62 | } 63 | NetworkObserverSoldier.shared.recordRequest(request: request, response: receivedResponse, responseData: receivedData, error: receivedError as NSError?, startDate: startDate) 64 | } 65 | 66 | //MARK: - URLSessionDelegate 67 | func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) { 68 | guard let redirectRequest = (request as NSURLRequest).mutableCopy() as? NSMutableURLRequest else { 69 | return 70 | } 71 | // The new request was copied from our old request, so it has our magic property. We actually 72 | // have to remove that so that, when the client starts the new request, we see it. If we 73 | // don't do this then we never see the new request and thus don't get a chance to change 74 | // its caching behaviour. 75 | // 76 | // We also cancel our current connection because the client is going to start a new request for 77 | // us anyway. 78 | URLProtocol.removeProperty(forKey: KJXCaptainURLProtocolIdentifier, in: redirectRequest) 79 | // Tell the client about the redirect. 80 | client?.urlProtocol(self, wasRedirectedTo: redirectRequest as URLRequest, redirectResponse: response) 81 | // Stop our load. The CFNetwork infrastructure will create a new NSURLProtocol instance to run 82 | // the load of the redirect. 83 | 84 | // The following ends up calling -URLSession:task:didCompleteWithError: with NSURLErrorDomain / NSURLErrorCancelled, 85 | // which specificallys traps and ignores the error. 86 | sessionTask?.cancel() 87 | let error = NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError, userInfo: nil) 88 | receivedError = error 89 | client?.urlProtocol(self, didFailWithError: error) 90 | } 91 | 92 | func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { 93 | client?.urlProtocol(self, didReceive: URLAuthenticationChallenge(authenticationChallenge: challenge, sender: JXCaptainURLSessionChallengeSender(completionHandler: completionHandler))) 94 | } 95 | 96 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { 97 | client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed) 98 | receivedData = Data() 99 | receivedResponse = response 100 | completionHandler(.allow) 101 | } 102 | 103 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, completionHandler: @escaping (CachedURLResponse?) -> Void) { 104 | completionHandler(proposedResponse) 105 | } 106 | 107 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { 108 | receivedData?.append(data) 109 | client?.urlProtocol(self, didLoad: data) 110 | } 111 | 112 | func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { 113 | if error == nil { 114 | recordRequest() 115 | client?.urlProtocolDidFinishLoading(self) 116 | }else if let localError = error as NSError? { 117 | receivedError = error 118 | if localError.domain == NSURLErrorDomain && localError.code == NSURLErrorCancelled { 119 | // Do nothing. This happens in two cases: 120 | // 121 | // o during a redirect, in which case the redirect code has already told the client about 122 | // the failure 123 | // 124 | // o if the request is cancelled by a call to -stopLoading, in which case the client doesn't 125 | // want to know about the failure 126 | }else { 127 | client?.urlProtocol(self, didFailWithError: error!) 128 | } 129 | } 130 | session.finishTasksAndInvalidate() 131 | } 132 | } 133 | 134 | extension URLSession { 135 | 136 | static let swizzleInit: Void = { 137 | let originalSelector = Selector(("initWithConfiguration:delegate:delegateQueue:")) 138 | let swizzledSelector = Selector(("initWithCaptainConfiguration:delegate:delegateQueue:")) 139 | let originalMethod = class_getInstanceMethod(URLSession.self, originalSelector) 140 | let swizzledMethod = class_getInstanceMethod(URLSession.self, swizzledSelector) 141 | guard originalMethod != nil, swizzledMethod != nil else { 142 | return 143 | } 144 | let didAddMethod = class_addMethod(URLSession.self, originalSelector, method_getImplementation(swizzledMethod!), method_getTypeEncoding(swizzledMethod!)) 145 | if didAddMethod { 146 | class_replaceMethod(URLSession.self, swizzledSelector, method_getImplementation(originalMethod!), method_getTypeEncoding(originalMethod!)) 147 | } else { 148 | method_exchangeImplementations(originalMethod!, swizzledMethod!); 149 | } 150 | }() 151 | 152 | @objc convenience init(captainConfiguration: URLSessionConfiguration, delegate: URLSessionDelegate?, delegateQueue queue: OperationQueue?) { 153 | self.init(captainConfiguration: captainConfiguration, delegate: delegate, delegateQueue: queue) 154 | 155 | let result = captainConfiguration.protocolClasses?.contains(where: { (type) -> Bool in 156 | if type == JXCaptainURLProtocol.self { 157 | return true 158 | }else { 159 | return false 160 | } 161 | }) 162 | if result != true { 163 | var protocols = captainConfiguration.protocolClasses 164 | protocols?.insert(JXCaptainURLProtocol.self, at: 0) 165 | captainConfiguration.protocolClasses = protocols 166 | } 167 | } 168 | } 169 | 170 | class JXCaptainURLSessionChallengeSender: NSObject, URLAuthenticationChallengeSender { 171 | let completionHandler: (URLSession.AuthChallengeDisposition, URLCredential?) -> Void 172 | 173 | init(completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { 174 | self.completionHandler = completionHandler 175 | super.init() 176 | } 177 | 178 | func use(_ credential: URLCredential, for challenge: URLAuthenticationChallenge) { 179 | completionHandler(.useCredential, credential) 180 | } 181 | 182 | func continueWithoutCredential(for challenge: URLAuthenticationChallenge) { 183 | completionHandler(.useCredential, nil) 184 | } 185 | 186 | func cancel(_ challenge: URLAuthenticationChallenge) { 187 | completionHandler(.cancelAuthenticationChallenge, nil) 188 | } 189 | 190 | func performDefaultHandling(for challenge: URLAuthenticationChallenge) { 191 | completionHandler(.performDefaultHandling, nil) 192 | } 193 | 194 | func rejectProtectionSpaceAndContinue(with challenge: URLAuthenticationChallenge) { 195 | completionHandler(.rejectProtectionSpace, nil) 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /Sources/Soldiers/NetworkObserver/NetworkObserverSoldier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkObserverSoldier.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/29. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import WebKit 11 | 12 | public class NetworkObserverSoldier: Soldier { 13 | public static let shared = NetworkObserverSoldier() 14 | public var name: String 15 | public var team: String 16 | public var icon: UIImage? 17 | /// responseData数据缓存最大容量,默认:50MB 18 | public var responseCacheByteLimit: Int = 50 * 1024 * 1024 { 19 | didSet { 20 | cache.countLimit = responseCacheByteLimit 21 | } 22 | } 23 | /// 是否能拦截WKWebView的接口,默认为false。 24 | /// 拦截的原理参考文章:https://blog.moecoder.com/2016/10/26/support-nsurlprotocol-in-wkwebview/ 因为WKWebView是一个单独的进程,如果要通过自定义的协议进行拦截,就会导致进程间通信,降低性能。所以,需要拦截WKWebView时,需要自己设置为true。 25 | public var canInterceptWKWebView: Bool = false 26 | var isActive: Bool { 27 | set { UserDefaults.standard.isNetworkObserverSoldierActive = newValue } 28 | get { UserDefaults.standard.isNetworkObserverSoldierActive } 29 | } 30 | var monitorView: MonitorConsoleLabel? 31 | let monitor: ANRMonitor 32 | var flowModels = [NetworkFlowModel]() 33 | let cache = NSCache() 34 | 35 | public init() { 36 | name = "流量" 37 | team = "性能检测" 38 | icon = ImageManager.imageWithName("JXCaptain_icon_network") 39 | monitor = ANRMonitor() 40 | cache.countLimit = responseCacheByteLimit 41 | } 42 | 43 | public func prepare() { 44 | if isActive { 45 | start() 46 | } 47 | } 48 | 49 | public func action(naviController: UINavigationController) { 50 | naviController.pushViewController(NetworkObserverDashboardViewController(soldier: self), animated: true) 51 | } 52 | 53 | func start() { 54 | if canInterceptWKWebView { 55 | WKWebView.register(schemes: ["http", "https"]) 56 | } 57 | URLProtocol.registerClass(JXCaptainURLProtocol.self) 58 | URLSession.swizzleInit 59 | isActive = true 60 | } 61 | 62 | func end() { 63 | if canInterceptWKWebView { 64 | WKWebView.unregister(schemes: ["http", "https"]) 65 | } 66 | URLProtocol.unregisterClass(JXCaptainURLProtocol.self) 67 | isActive = false 68 | } 69 | 70 | func recordRequest(request: URLRequest, response: URLResponse?, responseData: Data?, error: NSError?, startDate: Date) { 71 | let flowModel = NetworkFlowModel(request: request, response: response, responseData: responseData, error: error, startDate: startDate) 72 | flowModels.insert(flowModel, at: 0) 73 | if let data = responseData { 74 | cache.setObject(data as AnyObject, forKey: flowModel.requestID as AnyObject, cost: data.count) 75 | } 76 | DispatchQueue.main.async { 77 | NotificationCenter.default.post(name: NSNotification.Name.JXCaptainNetworkObserverSoldierNewFlowDidReceive, object: flowModel) 78 | } 79 | } 80 | } 81 | 82 | extension String { 83 | public var base64Decode: String? { 84 | guard let data = Data(base64Encoded: self) else { return nil } 85 | return String(data: data, encoding: .utf8) 86 | } 87 | public var base64Encode: String { 88 | return Data(utf8).base64EncodedString() 89 | } 90 | } 91 | 92 | extension WKWebView { 93 | private class func browsing_contextController() -> (NSObject.Type)? { 94 | guard let str = "YnJvd3NpbmdDb250ZXh0Q29udHJvbGxlcg==".base64Decode else { assertionFailure(); return nil } 95 | // str: "browsingContextController" 96 | guard let obj = WKWebView().value(forKey: str) else { return nil } 97 | return type(of: obj) as? NSObject.Type 98 | } 99 | 100 | private class func perform_browsing_contextController(aSelector: Selector, schemes: Set) -> Bool { 101 | guard let obj = browsing_contextController(), obj.responds(to: aSelector), schemes.count > 0 else { 102 | assertionFailure(); return false 103 | } 104 | var result = schemes.count > 0 105 | schemes.forEach({ (scheme) in 106 | let ret = obj.perform(aSelector, with: scheme) 107 | result = result && (ret != nil) 108 | }) 109 | return result 110 | } 111 | } 112 | 113 | extension WKWebView { 114 | @discardableResult public class func register(schemes: Set) -> Bool { 115 | guard let str = "cmVnaXN0ZXJTY2hlbWVGb3JDdXN0b21Qcm90b2NvbDo=".base64Decode else { 116 | assertionFailure(); return false 117 | } 118 | // str: "registerSchemeForCustomProtocol:" 119 | let register = NSSelectorFromString(str) 120 | return perform_browsing_contextController(aSelector: register, schemes: schemes) 121 | } 122 | 123 | @discardableResult public class func unregister(schemes: Set) -> Bool { 124 | guard let str = "dW5yZWdpc3RlclNjaGVtZUZvckN1c3RvbVByb3RvY29sOg==".base64Decode else { 125 | assertionFailure(); return false 126 | } 127 | //str: "unregisterSchemeForCustomProtocol:" 128 | let unregister = NSSelectorFromString(str) 129 | return perform_browsing_contextController(aSelector: unregister, schemes: schemes) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Sources/Soldiers/SanboxBrowser/SanboxBrowserSoldier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BundleBrowserSoldier.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/21. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class SanboxBrowserSoldier: Soldier { 12 | public var name: String 13 | public var team: String 14 | public var icon: UIImage? 15 | 16 | public init() { 17 | name = "沙盒浏览" 18 | team = "常用工具" 19 | icon = ImageManager.imageWithName("JXCaptain_icon_sanbox") 20 | } 21 | 22 | public func prepare() { 23 | } 24 | 25 | public func action(naviController: UINavigationController) { 26 | naviController.pushViewController(JXFileBrowserController(path: NSHomeDirectory()), animated: true) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Soldiers/UserDefaults/UserDefaultsSoldier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultsSoldier.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/9/9. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class UserDefaultsSoldier: Soldier { 12 | public static let shared = UserDefaultsSoldier() 13 | public var name: String 14 | public var team: String 15 | public var icon: UIImage? 16 | 17 | public init() { 18 | name = "UserDefaults" 19 | team = "常用工具" 20 | icon = ImageManager.imageWithName("JXCaptain_icon_app_user_defaults") 21 | } 22 | 23 | public func prepare() { } 24 | 25 | public func action(naviController: UINavigationController) { 26 | naviController.pushViewController(UserDefaultsKeyValuesListViewController(defaults: UserDefaults.standard), animated: true) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Soldiers/WebsiteEntry/WebsiteEntrySoldier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebsiteSoldier.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/23. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class WebsiteEntrySoldier: Soldier { 12 | public var name: String 13 | public var team: String 14 | public var icon: UIImage? 15 | public var defaultWebsite: String? 16 | public var webDetailControllerClosure: ((String)->(UIViewController))? 17 | 18 | deinit { 19 | webDetailControllerClosure = nil 20 | } 21 | 22 | public init() { 23 | name = "H5任意门" 24 | team = "常用工具" 25 | icon = ImageManager.imageWithName("JXCaptain_icon_h5") 26 | } 27 | 28 | public func prepare() { 29 | } 30 | 31 | public func action(naviController: UINavigationController) { 32 | naviController.pushViewController(WebsiteEntryViewController(soldier: self), animated: true) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/UI/ANR/ANRDashboardViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ANRDashboardViewController.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/28. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ANRDashboardViewController: UITableViewController { 12 | let soldier: ANRSoldier 13 | 14 | init(soldier: ANRSoldier) { 15 | self.soldier = soldier 16 | super.init(style: .plain) 17 | } 18 | 19 | required init?(coder aDecoder: NSCoder) { 20 | fatalError("init(coder:) has not been implemented") 21 | } 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | title = "卡顿监控" 27 | tableView.register(DashboardCell.self, forCellReuseIdentifier: "swithCell") 28 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") 29 | tableView.tableFooterView = UIView() 30 | 31 | if soldier.hasNewEvent { 32 | soldier.hasNewEvent = false 33 | NotificationCenter.default.post(name: .JXCaptainSoldierNewEventDidChange, object: soldier) 34 | } 35 | } 36 | 37 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 38 | return 3 39 | } 40 | 41 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 42 | if indexPath.row == 0 { 43 | let cell = tableView.dequeueReusableCell(withIdentifier: "swithCell", for: indexPath) as! DashboardCell 44 | cell.textLabel?.text = "卡顿检测开关" 45 | cell.toggle.isOn = soldier.isActive 46 | cell.toggleValueDidChange = {[weak self] (isOn) in 47 | if isOn { 48 | self?.soldier.start() 49 | }else { 50 | self?.soldier.end() 51 | } 52 | } 53 | return cell 54 | }else if indexPath.row == 1 { 55 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) 56 | cell.accessoryType = .disclosureIndicator 57 | cell.textLabel?.text = "查看卡顿日志" 58 | return cell 59 | }else { 60 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) 61 | cell.accessoryType = .disclosureIndicator 62 | cell.textLabel?.text = "清理卡顿日志" 63 | return cell 64 | } 65 | } 66 | 67 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 68 | tableView.deselectRow(at: indexPath, animated: true) 69 | if indexPath.row == 1 { 70 | let vc = ANRListViewController(dataSource: ANRFileManager.allFiles()) 71 | self.navigationController?.pushViewController(vc, animated: true) 72 | }else if indexPath.row == 2 { 73 | let alert = UIAlertController(title: "提示", message: "确认删除所有卡顿日志吗?", preferredStyle: .alert) 74 | alert.addAction(UIAlertAction(title: "取消", style: .cancel, handler: nil)) 75 | alert.addAction(UIAlertAction(title: "确定", style: .destructive, handler: { (action) in 76 | ANRFileManager.deleteAllFiles() 77 | })) 78 | present(alert, animated: true, completion: nil) 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/UI/ANR/ANRListViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ANRListViewController.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/28. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ANRListViewController: FileListViewController { 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | 15 | title = "ANR日志列表" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/UI/AppInfo/AppInfoViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppInfoViewController.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/22. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class AppInfoViewController: UITableViewController { 12 | var dataSource = [AppInfoSectionModel]() 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | 17 | title = "APP信息" 18 | 19 | tableView.register(AppInfoCell.self, forCellReuseIdentifier: "cell") 20 | tableView.tableFooterView = UIView() 21 | 22 | let phoneCellModels = [AppInfoCellModel(title: "手机型号", info: AppInfoManager.iphoneName()), 23 | AppInfoCellModel(title: "系统版本", info: AppInfoManager.iOSVersion())] 24 | let phoneInfo = AppInfoSectionModel(title: "手机信息", cellModels: phoneCellModels) 25 | let appCellModels = [AppInfoCellModel(title: "BundleID", info: AppInfoManager.bundleID()), 26 | AppInfoCellModel(title: "Version", info: AppInfoManager.bundleVersion()), 27 | AppInfoCellModel(title: "VersionCode", info: AppInfoManager.bundleCode())] 28 | let appInfo = AppInfoSectionModel(title: "APP信息", cellModels: appCellModels) 29 | let authorityCellModels = [AppInfoCellModel(title: "地理位置权限", info: AppInfoManager.locationAuthority()), 30 | AppInfoCellModel(title: "网络权限", info: AppInfoManager.networkAuthority()), 31 | AppInfoCellModel(title: "通知权限", info: AppInfoManager.notificationAuthority()), 32 | AppInfoCellModel(title: "相机权限", info: AppInfoManager.cameraAuthority()), 33 | AppInfoCellModel(title: "麦克风权限", info: AppInfoManager.audioAuthority()), 34 | AppInfoCellModel(title: "相册权限", info: AppInfoManager.cameraAuthority()), 35 | AppInfoCellModel(title: "通讯录权限", info: AppInfoManager.contactsAuthority()), 36 | AppInfoCellModel(title: "日历权限", info: AppInfoManager.calendarAuthority()), 37 | AppInfoCellModel(title: "提醒事项权限", info: AppInfoManager.reminderAuthority())] 38 | let authorityInfo = AppInfoSectionModel(title: "权限信息", cellModels: authorityCellModels) 39 | dataSource.append(contentsOf: [phoneInfo, appInfo, authorityInfo]) 40 | } 41 | 42 | override func numberOfSections(in tableView: UITableView) -> Int { 43 | return dataSource.count 44 | } 45 | 46 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 47 | return dataSource[section].cellModels.count 48 | } 49 | 50 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 51 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! AppInfoCell 52 | let cellModel = dataSource[indexPath.section].cellModels[indexPath.row] 53 | cell.titleLabel.text = cellModel.title 54 | cell.infoLabel.text = cellModel.info 55 | return cell 56 | } 57 | 58 | override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 59 | return dataSource[section].title 60 | } 61 | } 62 | 63 | struct AppInfoSectionModel { 64 | let title: String 65 | let cellModels: [AppInfoCellModel] 66 | } 67 | 68 | struct AppInfoCellModel { 69 | let title: String 70 | let info: String 71 | } 72 | 73 | class AppInfoCell: UITableViewCell { 74 | let titleLabel: UILabel 75 | let infoLabel: UILabel 76 | 77 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 78 | titleLabel = UILabel() 79 | infoLabel = UILabel() 80 | super.init(style: style, reuseIdentifier: reuseIdentifier) 81 | 82 | self.selectionStyle = .none 83 | 84 | titleLabel.font = .systemFont(ofSize: 17) 85 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 86 | contentView.addSubview(titleLabel) 87 | titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12).isActive = true 88 | titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true 89 | 90 | infoLabel.font = .systemFont(ofSize: 15) 91 | infoLabel.textColor = .gray 92 | infoLabel.translatesAutoresizingMaskIntoConstraints = false 93 | contentView.addSubview(infoLabel) 94 | infoLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -12).isActive = true 95 | infoLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true 96 | } 97 | 98 | required init?(coder aDecoder: NSCoder) { 99 | fatalError("init(coder:) has not been implemented") 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Sources/UI/CPU/CPUDashboardViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CPUDashboardViewController.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/28. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class CPUDashboardViewController: UITableViewController { 12 | let soldier: CPUSoldier 13 | 14 | init(soldier: CPUSoldier) { 15 | self.soldier = soldier 16 | super.init(style: .plain) 17 | } 18 | 19 | required init?(coder aDecoder: NSCoder) { 20 | fatalError("init(coder:) has not been implemented") 21 | } 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | title = "CPU" 27 | tableView.register(DashboardCell.self, forCellReuseIdentifier: "cell") 28 | tableView.tableFooterView = UIView() 29 | } 30 | 31 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 32 | return 1 33 | } 34 | 35 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 36 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! DashboardCell 37 | cell.textLabel?.text = "CPU检测开关" 38 | cell.toggle.isOn = soldier.isActive 39 | cell.toggleValueDidChange = {[weak self] (isOn) in 40 | if isOn { 41 | self?.soldier.start() 42 | }else { 43 | self?.soldier.end() 44 | } 45 | } 46 | return cell 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /Sources/UI/Common/DashboardCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DashboardCell.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/26. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class DashboardCell: UITableViewCell { 12 | let toggle: UISwitch 13 | var toggleValueDidChange: ((Bool)->())? 14 | 15 | deinit { 16 | toggleValueDidChange = nil 17 | } 18 | 19 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 20 | toggle = UISwitch() 21 | super.init(style: style, reuseIdentifier: reuseIdentifier) 22 | 23 | selectionStyle = .none 24 | textLabel?.backgroundColor = .clear 25 | toggle.translatesAutoresizingMaskIntoConstraints = false 26 | toggle.addTarget(self, action: #selector(toggleDidClick), for: .valueChanged) 27 | contentView.addSubview(toggle) 28 | toggle.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -12).isActive = true 29 | toggle.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true 30 | } 31 | 32 | required init?(coder aDecoder: NSCoder) { 33 | fatalError("init(coder:) has not been implemented") 34 | } 35 | 36 | @objc func toggleDidClick() { 37 | toggleValueDidChange?(toggle.isOn) 38 | } 39 | 40 | override func layoutSubviews() { 41 | super.layoutSubviews() 42 | 43 | contentView.bringSubviewToFront(toggle) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/UI/Common/FileListViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileListViewController.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/29. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class FileListViewController: UITableViewController { 12 | let dataSource: [SanboxModel] 13 | 14 | init(dataSource: [SanboxModel]) { 15 | self.dataSource = dataSource 16 | super.init(style: .plain) 17 | } 18 | 19 | required init?(coder aDecoder: NSCoder) { 20 | fatalError("init(coder:) has not been implemented") 21 | } 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") 27 | tableView.tableFooterView = UIView() 28 | if dataSource.isEmpty { 29 | let emptyLabel = UILabel() 30 | emptyLabel.font = .systemFont(ofSize: 25) 31 | emptyLabel.text = "暂无日志文件" 32 | emptyLabel.textAlignment = .center 33 | tableView.backgroundView = emptyLabel 34 | } 35 | } 36 | 37 | //MARK: - UITableViewDataSource & UITableViewDelegate 38 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 39 | return dataSource.count 40 | } 41 | 42 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 43 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) 44 | cell.accessoryType = .disclosureIndicator 45 | let model = dataSource[indexPath.row] 46 | cell.textLabel?.text = model.name 47 | return cell 48 | } 49 | 50 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 51 | tableView.deselectRow(at: indexPath, animated: true) 52 | let model = dataSource[indexPath.row] 53 | let sheet = UIAlertController(title: nil, message: "请选择操作方式", preferredStyle: .actionSheet) 54 | sheet.addAction(UIAlertAction(title: "本地预览", style: .default, handler: { (action) in 55 | let previewVC = JXFilePreviewViewController(filePath: model.fileURL.path) 56 | self.navigationController?.pushViewController(previewVC, animated: true) 57 | })) 58 | sheet.addAction(UIAlertAction(title: "分享", style: .default, handler: { (action) in 59 | let activityController = UIActivityViewController(activityItems: [model.fileURL], applicationActivities: nil) 60 | self.present(activityController, animated: true, completion: nil) 61 | })) 62 | sheet.addAction(UIAlertAction(title: "取消", style: .cancel, handler: nil)) 63 | present(sheet, animated: true, completion: nil) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/UI/Common/MonitorConsoleLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MonitorView.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/26. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class MonitorConsoleLabel: UILabel { 12 | 13 | override init(frame: CGRect) { 14 | super.init(frame: frame) 15 | 16 | textAlignment = .center 17 | } 18 | 19 | required init?(coder aDecoder: NSCoder) { 20 | fatalError("init(coder:) has not been implemented") 21 | } 22 | 23 | func update(type: MonitorType, value: Double) { 24 | switch type { 25 | case .fps: 26 | let percent = CGFloat(value/60) 27 | let valueColor = UIColor(hue: 0.27 * (percent - 0.2), saturation: 1, brightness: 0.9, alpha: 1) 28 | let contentText = "\(round(value)) FPS" 29 | let attrText = NSMutableAttributedString(string: contentText, attributes: [.foregroundColor : valueColor, .font : UIFont.systemFont(ofSize: 16)]) 30 | attrText.addAttribute(.foregroundColor, value: UIColor.white, range: NSString(string: contentText).range(of: "FPS")) 31 | attributedText = attrText 32 | case .memory: 33 | let percent = CGFloat(value/350) 34 | let valueColor = color(percent: percent) 35 | let contentText = String(format: "%.1f M", value) 36 | let attrText = NSMutableAttributedString(string: contentText, attributes: [.foregroundColor : valueColor, .font : UIFont.systemFont(ofSize: 16)]) 37 | attrText.addAttribute(.foregroundColor, value: UIColor.white, range: NSString(string: contentText).range(of: "M")) 38 | attributedText = attrText 39 | case .cpu: 40 | let percent = CGFloat(value/100) 41 | let valueColor = color(percent: percent) 42 | let contentText = String(format: "%.lf%% CPU", round(value)) 43 | let attrText = NSMutableAttributedString(string: contentText, attributes: [.foregroundColor : valueColor, .font : UIFont.systemFont(ofSize: 16)]) 44 | attrText.addAttribute(.foregroundColor, value: UIColor.white, range: NSString(string: contentText).range(of: "CPU")) 45 | attributedText = attrText 46 | default: break 47 | } 48 | } 49 | 50 | private func color(percent: CGFloat) -> UIColor { 51 | var r: CGFloat = 0 52 | var g: CGFloat = 0 53 | let one: CGFloat = 255 + 255 54 | if percent < 0.5 { 55 | r = one * percent 56 | g = 255 57 | }else if percent > 0.5 { 58 | g = 255 - ((percent - 0.5) * one) 59 | r = 255 60 | } 61 | return UIColor(red: r/255, green: g/255, blue: 0, alpha: 1) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/UI/Common/MonitorListWindow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MonitorListView.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/26. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class MonitorListWindow: UIWindow { 12 | static let `shared` = MonitorListWindow(frame: CGRect.zero) 13 | private var monitorViews: [UIView] = [UIView]() 14 | private let contentHeight: CGFloat = 20 15 | private let containerView = UIView() 16 | 17 | override init(frame: CGRect) { 18 | super.init(frame: UIScreen.main.bounds) 19 | 20 | self.windowLevel = .alert 21 | if rootViewController == nil { 22 | let vc = UIViewController() 23 | vc.view.backgroundColor = UIColor.clear 24 | let contentBGView = UIView() 25 | contentBGView.backgroundColor = UIColor.black 26 | var y: CGFloat = 0 27 | if #available(iOS 11.0, *) { 28 | if safeAreaInsets.top > 20 { 29 | y = 30 30 | } 31 | } 32 | contentBGView.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: y + contentHeight) 33 | vc.view.addSubview(contentBGView) 34 | containerView.frame = CGRect(x: 0, y: y, width: UIScreen.main.bounds.size.width, height: contentHeight) 35 | vc.view.addSubview(containerView) 36 | rootViewController = vc 37 | } 38 | } 39 | 40 | required init?(coder aDecoder: NSCoder) { 41 | fatalError("init(coder:) has not been implemented") 42 | } 43 | 44 | override func layoutSubviews() { 45 | super.layoutSubviews() 46 | 47 | backgroundColor = .clear 48 | } 49 | 50 | override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 51 | return nil 52 | } 53 | 54 | func show() { 55 | isHidden = false 56 | } 57 | 58 | func hide() { 59 | isHidden = true 60 | } 61 | 62 | func enqueue(monitorView: UIView) { 63 | show() 64 | containerView.addSubview(monitorView) 65 | monitorViews.append(monitorView) 66 | relayoutMonitorViews() 67 | } 68 | 69 | func dequeue(monitorView: UIView) { 70 | for index in 0.. Int { 38 | return 2 39 | } 40 | 41 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 42 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) 43 | cell.accessoryType = .disclosureIndicator 44 | if indexPath.row == 0 { 45 | cell.textLabel?.text = "查看Crash日志" 46 | }else if indexPath.row == 1 { 47 | cell.textLabel?.text = "清理Crash日志" 48 | } 49 | return cell 50 | } 51 | 52 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 53 | tableView.deselectRow(at: indexPath, animated: true) 54 | if indexPath.row == 0 { 55 | let vc = CrashListViewController(dataSource: CrashFileManager.allFiles()) 56 | self.navigationController?.pushViewController(vc, animated: true) 57 | }else if indexPath.row == 1 { 58 | let alert = UIAlertController(title: "提示", message: "确认删除所有Crash日志吗?", preferredStyle: .alert) 59 | alert.addAction(UIAlertAction(title: "取消", style: .cancel, handler: nil)) 60 | alert.addAction(UIAlertAction(title: "确定", style: .destructive, handler: { (action) in 61 | CrashFileManager.deleteAllFiles() 62 | })) 63 | present(alert, animated: true, completion: nil) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/UI/Crash/CrashListViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CrashListViewController.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/21. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class CrashListViewController: FileListViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | 16 | title = "Crash日志列表" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/UI/FPS/FPSDashboardViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FPSDashboardViewController.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/26. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class FPSDashboardViewController: UITableViewController { 12 | let soldier: FPSSoldier 13 | 14 | init(soldier: FPSSoldier) { 15 | self.soldier = soldier 16 | super.init(style: .plain) 17 | } 18 | 19 | required init?(coder aDecoder: NSCoder) { 20 | fatalError("init(coder:) has not been implemented") 21 | } 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | title = "FPS" 27 | tableView.register(DashboardCell.self, forCellReuseIdentifier: "cell") 28 | tableView.tableFooterView = UIView() 29 | } 30 | 31 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 32 | return 1 33 | } 34 | 35 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 36 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! DashboardCell 37 | cell.textLabel?.text = "FPS检测开关" 38 | cell.toggle.isOn = soldier.isActive 39 | cell.toggleValueDidChange = {[weak self] (isOn) in 40 | if isOn { 41 | self?.soldier.start() 42 | }else { 43 | self?.soldier.end() 44 | } 45 | } 46 | return cell 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/UI/Main/BaseTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseTableViewController.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/9/9. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class BaseTableViewController: UITableViewController { 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | 15 | view.backgroundColor = .white 16 | } 17 | 18 | override var preferredStatusBarStyle: UIStatusBarStyle { 19 | return .default 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/UI/Main/BaseViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseViewController.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/23. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class BaseViewController: UIViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | 16 | view.backgroundColor = .white 17 | } 18 | 19 | override var preferredStatusBarStyle: UIStatusBarStyle { 20 | return .default 21 | } 22 | } 23 | 24 | class BaseNavigationController: UINavigationController { 25 | override var childForStatusBarStyle: UIViewController? { 26 | return topController() 27 | } 28 | 29 | override var childForStatusBarHidden: UIViewController? { 30 | return topController() 31 | } 32 | } 33 | 34 | extension UIViewController { 35 | func topController() -> UIViewController? { 36 | if let navi = self as? UINavigationController { 37 | return navi.topViewController?.topController() 38 | }else if let tabbar = self as? UITabBarController { 39 | return tabbar.selectedViewController?.topController() 40 | }else if presentedViewController != nil { 41 | return presentedViewController?.topController() 42 | }else { 43 | return self 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/UI/Main/CaptainFloatingViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CaptainFloatingViewController.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/21. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class CaptainFloatingViewController: BaseViewController { 12 | var shieldButton: UIButton! 13 | let shieldWidth: CGFloat = 50 14 | var newEventView: UIView! 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | 19 | view.backgroundColor = .clear 20 | 21 | shieldButton = UIButton(type: .custom) 22 | shieldButton.setImage(Captain.default.logoImage, for: .normal) 23 | shieldButton.layer.shadowOpacity = 0.6 24 | shieldButton.layer.shadowColor = UIColor.black.cgColor 25 | shieldButton.layer.shadowRadius = 3 26 | shieldButton.layer.shadowOffset = CGSize.zero 27 | let screenEdgeInsets = Captain.default.screenEdgeInsets 28 | let y = screenEdgeInsets.top + (view.bounds.size.height - screenEdgeInsets.top - screenEdgeInsets.bottom - shieldWidth)/2 29 | shieldButton.frame = CGRect(x: screenEdgeInsets.left, y: y, width: shieldWidth, height: shieldWidth) 30 | shieldButton.addTarget(self, action: #selector(shieldButtonDidClick), for: .touchUpInside) 31 | view.addSubview(shieldButton) 32 | 33 | newEventView = UIView() 34 | newEventView.isHidden = true 35 | newEventView.backgroundColor = UIColor.red 36 | newEventView.layer.cornerRadius = 4 37 | newEventView.frame = CGRect(x: shieldButton.bounds.size.width - 8, y: 0, width: 8, height: 8) 38 | shieldButton.addSubview(newEventView) 39 | refreshNewEventView() 40 | 41 | let pan = UIPanGestureRecognizer(target: self, action: #selector(processPan(_:))) 42 | shieldButton.addGestureRecognizer(pan) 43 | 44 | NotificationCenter.default.addObserver(self, selector: #selector(soldierNewEventDidChange), name: .JXCaptainSoldierNewEventDidChange, object: nil) 45 | //FIXME:如果要保证当前的状态栏跟目标APP当前页面一直,可以不断触发setNeedsStatusBarAppearanceUpdate方法,但是经过测试会带来额外的6%CPU开销。 46 | // let link = CADisplayLink(target: self, selector: #selector(processLink)) 47 | // link.add(to: RunLoop.current, forMode: .common) 48 | } 49 | 50 | @objc func processLink() { 51 | setNeedsStatusBarAppearanceUpdate() 52 | } 53 | 54 | override var preferredStatusBarStyle: UIStatusBarStyle { 55 | if UIApplication.shared.keyWindow?.rootViewController?.topController() == self { 56 | return .default 57 | }else { 58 | return UIApplication.shared.keyWindow?.rootViewController?.topController()?.preferredStatusBarStyle ?? .default 59 | } 60 | } 61 | 62 | @objc func shieldButtonDidClick() { 63 | let navi = BaseNavigationController(rootViewController: SoldierListViewController()) 64 | navi.modalPresentationStyle = .fullScreen 65 | present(navi, animated: true, completion: nil) 66 | } 67 | 68 | @objc func soldierNewEventDidChange() { 69 | DispatchQueue.main.async { 70 | self.refreshNewEventView() 71 | } 72 | } 73 | 74 | func refreshNewEventView() { 75 | newEventView.isHidden = true 76 | for soldier in Captain.default.soldiers { 77 | if soldier.hasNewEvent { 78 | newEventView.isHidden = false 79 | break 80 | } 81 | } 82 | } 83 | 84 | @objc func processPan(_ gesture: UIPanGestureRecognizer) { 85 | let point = gesture.location(in: view) 86 | let screenEdgeInsets = Captain.default.screenEdgeInsets 87 | let minCenterX = screenEdgeInsets.left + shieldWidth/2 88 | let maxCenterX = view.bounds.size.width - screenEdgeInsets.right - shieldWidth/2 89 | let minCenterY = screenEdgeInsets.top + shieldWidth/2 90 | let maxCenterY = view.bounds.size.height - screenEdgeInsets.bottom - shieldWidth/2 91 | let centerX = min(maxCenterX, max(minCenterX, point.x)) 92 | let centerY = min(maxCenterY, max(minCenterY, point.y)) 93 | if gesture.state == .began { 94 | UIView.animate(withDuration: 0.1) { 95 | self.shieldButton.center = point 96 | } 97 | }else if gesture.state == .changed { 98 | shieldButton.center = CGPoint(x:centerX , y: centerY) 99 | }else if gesture.state == .ended || gesture.state == .cancelled { 100 | UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseOut, animations: { 101 | var center = self.shieldButton.center 102 | if center.x > self.view.bounds.size.width/2 { 103 | center.x = maxCenterX 104 | }else { 105 | center.x = minCenterX 106 | } 107 | self.shieldButton.center = center 108 | }, completion: nil) 109 | } 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /Sources/UI/Main/CaptainFloatingWindow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // floatingWindow.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/21. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class CaptainFloatingWindow: UIWindow { 12 | let floatingVC: CaptainFloatingViewController 13 | 14 | override init(frame: CGRect) { 15 | floatingVC = CaptainFloatingViewController() 16 | super.init(frame: UIScreen.main.bounds) 17 | 18 | windowLevel = UIWindow.Level(rawValue: UIWindow.Level.statusBar.rawValue - 1) 19 | rootViewController = floatingVC 20 | backgroundColor = .clear 21 | } 22 | 23 | required init?(coder aDecoder: NSCoder) { 24 | fatalError("init(coder:) has not been implemented") 25 | } 26 | 27 | override func layoutSubviews() { 28 | super.layoutSubviews() 29 | 30 | backgroundColor = .clear 31 | } 32 | 33 | override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 34 | let view = super.hitTest(point, with: event) 35 | if view == floatingVC.view { 36 | return nil 37 | } 38 | return view 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/UI/Memory/MemoryDashboardViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MemoryDashboardViewController.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/26. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class MemoryDashboardViewController: UITableViewController { 12 | let soldier: MemorySoldier 13 | 14 | init(soldier: MemorySoldier) { 15 | self.soldier = soldier 16 | super.init(style: .plain) 17 | } 18 | 19 | required init?(coder aDecoder: NSCoder) { 20 | fatalError("init(coder:) has not been implemented") 21 | } 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | title = "内存" 27 | tableView.register(DashboardCell.self, forCellReuseIdentifier: "cell") 28 | tableView.tableFooterView = UIView() 29 | } 30 | 31 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 32 | return 1 33 | } 34 | 35 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 36 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! DashboardCell 37 | cell.textLabel?.text = "内存检测开关" 38 | cell.toggle.isOn = soldier.isActive 39 | cell.toggleValueDidChange = {[weak self] (isOn) in 40 | if isOn { 41 | self?.soldier.start() 42 | }else { 43 | self?.soldier.end() 44 | } 45 | } 46 | return cell 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /Sources/UI/Model/NetworkFlowDetailCellModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkFlowDetailCellModel.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/9/2. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum NetworkFlowDetailCellType { 12 | case normal 13 | case requestBody 14 | case responseBody 15 | } 16 | 17 | struct NetworkFlowDetailCellModel { 18 | let type: NetworkFlowDetailCellType 19 | let text: NSAttributedString 20 | 21 | init(type: NetworkFlowDetailCellType = .normal, text: NSAttributedString) { 22 | self.type = type 23 | self.text = text 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/UI/Model/NetworkFlowDetailSectionModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkFlowDetailSectionModel.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/9/2. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct NetworkFlowDetailSectionModel { 12 | let title: String 13 | let items: [NetworkFlowDetailCellModel] 14 | } 15 | -------------------------------------------------------------------------------- /Sources/UI/Model/NetworkFlowModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkTransaction.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/29. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | //@property (nonatomic, copy) NSString *requestId; 12 | struct NetworkFlowModel { 13 | let requestID: String 14 | let request: URLRequest 15 | let response: URLResponse? 16 | let error: NSError? 17 | 18 | let statusCode: Int? 19 | let startDate: Date 20 | let endDate: Date 21 | let duration: TimeInterval //单位秒 22 | 23 | let requestBodyString: String 24 | let requestBodySize: String 25 | let urlString: String? 26 | let method: String 27 | let mimeType: String 28 | let uploadFlow: String 29 | let downFlow: String 30 | let durationString: String 31 | let startDateString: String 32 | let statusCodeString: String 33 | let errorString: String? 34 | let isStatusCodeError: Bool 35 | let isImageResponseData: Bool 36 | let isGif: Bool 37 | let mediaFileName: String 38 | let isVedio: Bool 39 | let isAudio: Bool 40 | 41 | init(request: URLRequest, response: URLResponse?, responseData: Data?, error: NSError?, startDate: Date) { 42 | self.requestID = UUID().uuidString 43 | self.request = request 44 | self.response = response 45 | self.startDate = startDate 46 | self.error = error 47 | 48 | let defaultString = "Unknown" 49 | requestBodyString = NetworkManager.jsonString(from: NetworkManager.httpBody(request: request) ?? Data()) ?? defaultString 50 | if response != nil && responseData != nil { 51 | mimeType = response!.mimeType ?? defaultString 52 | downFlow = NetworkManager.flowLengthString(NetworkManager.responseFlowLength(response!, responseData: responseData!)) 53 | }else { 54 | mimeType = defaultString 55 | downFlow = defaultString 56 | } 57 | isImageResponseData = mimeType.hasPrefix("image/") 58 | isGif = mimeType.contains("gif") 59 | isVedio = mimeType.hasPrefix("video/") 60 | isAudio = mimeType.hasPrefix("audio/") 61 | if request.url?.pathExtension.isEmpty == false { 62 | mediaFileName = request.url?.pathComponents.last ?? "unknown" 63 | }else { 64 | if let disposition = (response as? HTTPURLResponse)?.allHeaderFields["Content-Disposition"] as? String { 65 | let scanner = Scanner(string: disposition) 66 | scanner.scanUpTo("filename=\"", into: nil) 67 | scanner.scanString("filename=\"", into: nil) 68 | var result: NSString? 69 | scanner.scanUpTo("\"", into: &result) 70 | if result != nil { 71 | mediaFileName = result! as String 72 | }else { 73 | mediaFileName = "unknown" 74 | } 75 | }else { 76 | mediaFileName = "unknown" 77 | } 78 | } 79 | 80 | errorString = error?.localizedDescription 81 | urlString = request.url?.absoluteString 82 | method = request.httpMethod ?? defaultString 83 | statusCode = (response as? HTTPURLResponse)?.statusCode 84 | if statusCode != nil { 85 | statusCodeString = "\(statusCode!) \(HTTPURLResponse.localizedString(forStatusCode: statusCode!))" 86 | let errorStatusCodes = IndexSet(integersIn: Range.init(NSRange(location: 400, length: 200))!) 87 | isStatusCodeError = errorStatusCodes.contains(statusCode!) 88 | }else { 89 | statusCodeString = defaultString 90 | isStatusCodeError = false 91 | } 92 | endDate = Date() 93 | duration = endDate.timeIntervalSince(startDate) 94 | if duration > 1 { 95 | durationString = String(format: "%.1fs", duration) 96 | }else { 97 | durationString = String(format: "%.fms", duration * 1000) 98 | } 99 | let dateFormatter = DateFormatter() 100 | dateFormatter.timeZone = TimeZone.current 101 | dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" 102 | startDateString = dateFormatter.string(from: startDate) 103 | requestBodySize = NetworkManager.flowLengthString(NetworkManager.httpBody(request: request)?.count ?? 0) 104 | uploadFlow = NetworkManager.flowLengthString(NetworkManager.requestFlowLength(request)) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/UI/Model/SanboxModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SanboxModel.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/21. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct SanboxModel { 12 | public let fileURL: URL 13 | public let name: String 14 | public init(fileURL: URL, name: String) { 15 | self.fileURL = fileURL 16 | self.name = name 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/UI/Model/SoldierListSectionModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SoldierListCellModel.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/22. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct SoldierListSectionModel { 12 | let teamName: String 13 | let soldiers: [Soldier] 14 | } 15 | -------------------------------------------------------------------------------- /Sources/UI/Model/UserDefaultsKeyValueModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultsKeyValueModel.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/9/9. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum UserDefaultsVaule { 12 | case string(String) 13 | case number(NSNumber) 14 | case date(Date) 15 | case data(Data) 16 | case array([Any]) 17 | case dictionary([String:Any]) 18 | } 19 | 20 | struct UserDefaultsKeyValueModel { 21 | let key: String 22 | let value: UserDefaultsVaule 23 | 24 | init(key: String, value: Any, userDefaults: UserDefaults) { 25 | self.key = key 26 | if let date = userDefaults.object(forKey: key) as? Date { 27 | self.value = .date(date) 28 | }else if let array = userDefaults.array(forKey: key) { 29 | self.value = .array(array) 30 | }else if let dict = userDefaults.dictionary(forKey: key) { 31 | self.value = .dictionary(dict) 32 | }else if let string = userDefaults.string(forKey: key) { 33 | self.value = .string(string) 34 | }else if let data = userDefaults.data(forKey: key) { 35 | self.value = .data(data) 36 | }else if let number = userDefaults.object(forKey: key) as? NSNumber { 37 | self.value = .number(number) 38 | }else { 39 | self.value = .string("unknown") 40 | } 41 | } 42 | 43 | func valueDescription() -> String { 44 | switch value { 45 | case .date(let date): 46 | return date.description 47 | case .array(let array): 48 | return array.description 49 | case .dictionary(let dict): 50 | return dict.description 51 | case .string(let string): 52 | return string 53 | case .data(let data): 54 | return data.description 55 | case .number(let number): 56 | return number.description 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/UI/NetworkObserver/NetworkFlowDetailViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkFlowDetailViewController.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/9/2. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class NetworkFlowDetailViewController: UITableViewController { 12 | let flowModel: NetworkFlowModel 13 | var dataSource = [NetworkFlowDetailSectionModel]() 14 | 15 | init(flowModel: NetworkFlowModel) { 16 | self.flowModel = flowModel 17 | super.init(style: .grouped) 18 | 19 | let request = flowModel.request 20 | let response = flowModel.response 21 | var responseBodyCellModel: NetworkFlowDetailCellModel? 22 | if NetworkObserverSoldier.shared.cache.object(forKey: flowModel.requestID as AnyObject) == nil { 23 | responseBodyCellModel = cellModel(title: "Response Body", detail: "no in cache") 24 | }else { 25 | responseBodyCellModel = cellModel(type: .responseBody, title: "Response Body", detail: "tap to view") 26 | } 27 | var generalItems = [cellModel(title: "Request URL", detail: request.url?.absoluteString), 28 | cellModel(title: "Request Method", detail: request.httpMethod), 29 | cellModel(title: "Request Body Size", detail: flowModel.requestBodySize), 30 | cellModel(type: .requestBody, title: "Request Body", detail: "tap to view"), 31 | cellModel(title: "Status Code", detail: flowModel.statusCodeString), 32 | responseBodyCellModel!, 33 | cellModel(title: "Response Size", detail: flowModel.downFlow), 34 | cellModel(title: "MIME Type", detail: flowModel.mimeType), 35 | cellModel(title: "Start Time", detail: flowModel.startDateString), 36 | cellModel(title: "Total Duration", detail: flowModel.durationString)] 37 | if flowModel.errorString != nil { 38 | generalItems.insert(cellModel(title: "Error", detail: flowModel.errorString), at: 2) 39 | } 40 | let generalSection = NetworkFlowDetailSectionModel(title: "General", items: generalItems) 41 | dataSource.append(generalSection) 42 | 43 | if let headerFileds = request.allHTTPHeaderFields, !headerFileds.isEmpty { 44 | var requestItems = [NetworkFlowDetailCellModel]() 45 | for (key, value) in headerFileds { 46 | requestItems.append(cellModel(title: key, detail: value)) 47 | } 48 | dataSource.append(NetworkFlowDetailSectionModel(title: "Request Headers", items: requestItems)) 49 | } 50 | 51 | if let headerFileds = (response as? HTTPURLResponse)?.allHeaderFields, !headerFileds.isEmpty { 52 | var responseItems = [NetworkFlowDetailCellModel]() 53 | for (key, value) in headerFileds { 54 | guard let keyString = key as? String else { 55 | continue 56 | } 57 | if let valueString = value as? String { 58 | responseItems.append(cellModel(title: keyString, detail: valueString)) 59 | }else if let valueArray = value as? [String] { 60 | let valueString = valueArray.reduce("") { (result, item) -> String in 61 | return "\(result),\(item)" 62 | } 63 | responseItems.append(cellModel(title: keyString, detail: valueString)) 64 | }else if let valueDict = value as? [String : String] { 65 | let valueString = valueDict.reduce("") { (result, valueTuple) -> String in 66 | return "\(result),\(valueTuple.key)=\(valueTuple.value)" 67 | } 68 | responseItems.append(cellModel(title: keyString, detail: valueString)) 69 | } 70 | } 71 | dataSource.append(NetworkFlowDetailSectionModel(title: "Response Headers", items: responseItems)) 72 | } 73 | } 74 | 75 | required init?(coder aDecoder: NSCoder) { 76 | fatalError("init(coder:) has not been implemented") 77 | } 78 | 79 | override func viewDidLoad() { 80 | super.viewDidLoad() 81 | 82 | title = "请求详情" 83 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") 84 | } 85 | 86 | func cellModel(type: NetworkFlowDetailCellType = .normal, title: String, detail: String?) -> NetworkFlowDetailCellModel { 87 | return NetworkFlowDetailCellModel(type: type, text: cellText(title: title, detail: detail)) 88 | } 89 | 90 | func cellText(title: String, detail: String?) -> NSAttributedString { 91 | let wholeString = "\(title):\(detail ?? "unknown")" 92 | let text = NSMutableAttributedString(string: wholeString, attributes: [NSAttributedString.Key.font : UIFont.systemFont(ofSize: 13), NSAttributedString.Key.foregroundColor : UIColor.black]) 93 | text.addAttributes([NSAttributedString.Key.font : UIFont.systemFont(ofSize: 15, weight: .medium), NSAttributedString.Key.foregroundColor : UIColor.gray], range: NSString(string: wholeString).range(of: "\(title):")) 94 | return text 95 | } 96 | 97 | //MARK: - UITableViewDataSource & UITableViewDelegate 98 | override func numberOfSections(in tableView: UITableView) -> Int { 99 | return dataSource.count 100 | } 101 | 102 | override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 103 | return dataSource[section].title 104 | } 105 | 106 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 107 | return dataSource[section].items.count 108 | } 109 | 110 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 111 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) 112 | let cellModel = dataSource[indexPath.section].items[indexPath.row] 113 | if cellModel.type == .normal { 114 | cell.accessoryType = .none 115 | cell.selectionStyle = .none 116 | }else { 117 | cell.accessoryType = .disclosureIndicator 118 | cell.selectionStyle = .default 119 | } 120 | cell.textLabel?.numberOfLines = 0 121 | cell.textLabel?.attributedText = cellModel.text 122 | return cell 123 | } 124 | 125 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 126 | tableView.deselectRow(at: indexPath, animated: true) 127 | UIMenuController.shared.setMenuVisible(false, animated: true) 128 | let cellModel = dataSource[indexPath.section].items[indexPath.row] 129 | if cellModel.type != .normal { 130 | let vc = NetworkFlowResponseDataDetailViewController(flowModel: flowModel, cellType: cellModel.type) 131 | navigationController?.pushViewController(vc, animated: true) 132 | } 133 | } 134 | 135 | override func tableView(_ tableView: UITableView, shouldShowMenuForRowAt indexPath: IndexPath) -> Bool { 136 | return true 137 | } 138 | 139 | override func tableView(_ tableView: UITableView, canPerformAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) -> Bool { 140 | if action == #selector(copy(_:)) { 141 | return true 142 | }else { 143 | return false 144 | } 145 | } 146 | 147 | override func tableView(_ tableView: UITableView, performAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) { 148 | if action == #selector(copy(_:)) { 149 | let cellModel = dataSource[indexPath.section].items[indexPath.row] 150 | UIPasteboard.general.string = cellModel.text.string 151 | } 152 | } 153 | 154 | override func scrollViewDidScroll(_ scrollView: UIScrollView) { 155 | UIMenuController.shared.setMenuVisible(false, animated: true) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /Sources/UI/NetworkObserver/NetworkFlowListViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkFlowListViewController.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/30. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class NetworkFlowListViewController: UITableViewController { 12 | let soldier: NetworkObserverSoldier 13 | var dataSource: [NetworkFlowModel] 14 | var searchController: UISearchController! 15 | var filteredDataSource = [NetworkFlowModel]() 16 | 17 | init(soldier: NetworkObserverSoldier) { 18 | self.soldier = soldier 19 | dataSource = soldier.flowModels 20 | super.init(style: .plain) 21 | 22 | } 23 | 24 | required init?(coder aDecoder: NSCoder) { 25 | fatalError("init(coder:) has not been implemented") 26 | } 27 | 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | 31 | title = "请求列表" 32 | 33 | searchController = UISearchController(searchResultsController: nil) 34 | searchController.searchBar.placeholder = "搜索URL" 35 | searchController.hidesNavigationBarDuringPresentation = false 36 | searchController.searchResultsUpdater = self 37 | searchController.dimsBackgroundDuringPresentation = false 38 | 39 | tableView.register(NetworkFlowListCell.self, forCellReuseIdentifier: "cell") 40 | tableView.tableHeaderView = searchController.searchBar 41 | 42 | NotificationCenter.default.addObserver(self, selector: #selector(newFlowDidReceive(noti:)), name: NSNotification.Name.JXCaptainNetworkObserverSoldierNewFlowDidReceive, object: nil) 43 | } 44 | 45 | @objc func newFlowDidReceive(noti: Notification) { 46 | guard let flowModel = noti.object as? NetworkFlowModel else { 47 | return 48 | } 49 | dataSource.insert(flowModel, at: 0) 50 | if searchController.isActive, let searchText = searchController.searchBar.text { 51 | if flowModel.request.url?.absoluteString.range(of: searchText, options: .caseInsensitive) != nil { 52 | filteredDataSource.insert(flowModel, at: 0) 53 | tableView.insertRows(at: [IndexPath(row: 0, section: 0)], with: .top) 54 | } 55 | }else { 56 | tableView.insertRows(at: [IndexPath(row: 0, section: 0)], with: .top) 57 | } 58 | } 59 | 60 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 61 | if searchController.isActive { 62 | return filteredDataSource.count 63 | }else { 64 | return dataSource.count 65 | } 66 | } 67 | 68 | override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 69 | return 55 70 | } 71 | 72 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 73 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! NetworkFlowListCell 74 | var flowModel: NetworkFlowModel! 75 | if searchController.isActive { 76 | flowModel = filteredDataSource[indexPath.row] 77 | }else { 78 | flowModel = dataSource[indexPath.row] 79 | } 80 | if flowModel.errorString != nil { 81 | cell.urlLabel.textColor = .red 82 | cell.infoLabel.text = "\(flowModel.method) · \(flowModel.downFlow) · \(flowModel.durationString) · \(flowModel.startDateString)" 83 | }else { 84 | cell.urlLabel.textColor = .black 85 | let infoString = "\(flowModel.method) · \(flowModel.statusCode ?? -1) · \(flowModel.downFlow) · \(flowModel.durationString) · \(flowModel.startDateString)" 86 | if flowModel.isStatusCodeError { 87 | let statusCodeString = "\(flowModel.statusCode ?? -1)" 88 | let infoText = NSMutableAttributedString(string: infoString, attributes: [NSAttributedString.Key.foregroundColor : UIColor.gray]) 89 | infoText.addAttribute(.foregroundColor, value: UIColor.red, range: NSString(string: infoString).range(of: statusCodeString)) 90 | cell.infoLabel.attributedText = infoText 91 | }else { 92 | cell.infoLabel.text = infoString 93 | } 94 | } 95 | cell.urlLabel.text = flowModel.urlString 96 | return cell 97 | } 98 | 99 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 100 | tableView.deselectRow(at: indexPath, animated: true) 101 | var flowModel: NetworkFlowModel! 102 | if searchController.isActive { 103 | flowModel = filteredDataSource[indexPath.row] 104 | }else { 105 | flowModel = dataSource[indexPath.row] 106 | } 107 | searchController.isActive = false 108 | let vc = NetworkFlowDetailViewController(flowModel: flowModel) 109 | navigationController?.pushViewController(vc, animated: true) 110 | } 111 | } 112 | 113 | extension NetworkFlowListViewController: UISearchResultsUpdating { 114 | func updateSearchResults(for searchController: UISearchController) { 115 | if searchController.searchBar.text?.isEmpty == false { 116 | filteredDataSource.removeAll() 117 | for flowModel in dataSource { 118 | if flowModel.urlString?.range(of: searchController.searchBar.text!, options: .caseInsensitive) != nil { 119 | filteredDataSource.append(flowModel) 120 | } 121 | } 122 | }else { 123 | filteredDataSource = dataSource 124 | } 125 | tableView.reloadData() 126 | } 127 | } 128 | 129 | 130 | class NetworkFlowListCell: UITableViewCell { 131 | let urlLabel: UILabel 132 | let infoLabel: UILabel 133 | 134 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 135 | urlLabel = UILabel() 136 | infoLabel = UILabel() 137 | super.init(style: style, reuseIdentifier: reuseIdentifier) 138 | 139 | accessoryType = .disclosureIndicator 140 | 141 | urlLabel.textColor = .black 142 | urlLabel.font = .systemFont(ofSize: 13) 143 | urlLabel.translatesAutoresizingMaskIntoConstraints = false 144 | urlLabel.numberOfLines = 2 145 | contentView.addSubview(urlLabel) 146 | urlLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5).isActive = true 147 | urlLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12).isActive = true 148 | urlLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -12).isActive = true 149 | 150 | infoLabel.textColor = .gray 151 | infoLabel.font = .systemFont(ofSize: 10) 152 | infoLabel.translatesAutoresizingMaskIntoConstraints = false 153 | infoLabel.numberOfLines = 1 154 | contentView.addSubview(infoLabel) 155 | infoLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5).isActive = true 156 | infoLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12).isActive = true 157 | infoLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -12).isActive = true 158 | } 159 | 160 | required init?(coder aDecoder: NSCoder) { 161 | fatalError("init(coder:) has not been implemented") 162 | } 163 | 164 | override func prepareForReuse() { 165 | super.prepareForReuse() 166 | 167 | infoLabel.attributedText = nil 168 | infoLabel.text = nil 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /Sources/UI/NetworkObserver/NetworkFlowResponseDataDetailViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkFlowDetailTextViewController.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/9/2. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AVFoundation 11 | import AVKit 12 | 13 | class NetworkFlowResponseDataDetailViewController: BaseViewController, UIScrollViewDelegate { 14 | let flowModel: NetworkFlowModel 15 | let cellType: NetworkFlowDetailCellType 16 | var text: String? 17 | var image: UIImage? 18 | var mediaURL: URL? 19 | var previewImageView: UIImageView? 20 | var previewScrollView: UIScrollView? 21 | var previewTextView: UITextView? 22 | var previewPlayerController: AVPlayerViewController? 23 | 24 | init(flowModel: NetworkFlowModel, cellType: NetworkFlowDetailCellType) { 25 | self.flowModel = flowModel 26 | self.cellType = cellType 27 | self.text = nil 28 | self.image = nil 29 | self.mediaURL = nil 30 | if cellType == .requestBody { 31 | self.text = flowModel.requestBodyString 32 | }else if cellType == .responseBody { 33 | if flowModel.isImageResponseData { 34 | self.image = NetworkManager.responseImage(requestID: flowModel.requestID) 35 | }else if flowModel.isVedio || flowModel.isAudio { 36 | let mediaData = NetworkManager.responseData(requestID: flowModel.requestID) 37 | let tempDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) 38 | let tempMediaURL = tempDirectoryURL.appendingPathComponent(flowModel.mediaFileName) 39 | if (try? mediaData?.write(to: tempMediaURL)) != nil { 40 | self.mediaURL = tempMediaURL 41 | } 42 | }else { 43 | self.text = NetworkManager.responseJSON(requestID: flowModel.requestID) 44 | } 45 | } 46 | super.init(nibName: nil, bundle: nil) 47 | } 48 | 49 | required init?(coder aDecoder: NSCoder) { 50 | fatalError("init(coder:) has not been implemented") 51 | } 52 | 53 | override func viewDidLoad() { 54 | super.viewDidLoad() 55 | 56 | if cellType == .requestBody { 57 | title = "Request" 58 | }else { 59 | title = "Response" 60 | } 61 | view.backgroundColor = .white 62 | 63 | if image != nil { 64 | previewScrollView = UIScrollView() 65 | previewScrollView?.delegate = self 66 | previewScrollView?.minimumZoomScale = 1 67 | var imageWidthScale: CGFloat = 1 68 | if view.bounds.size.width > 0 { 69 | imageWidthScale = (image?.size.width ?? 0)/view.bounds.size.width 70 | } 71 | previewScrollView?.maximumZoomScale = max(2, imageWidthScale) 72 | view.addSubview(previewScrollView!) 73 | 74 | previewImageView = UIImageView() 75 | if flowModel.isGif { 76 | previewImageView?.animationImages = NetworkManager.responseImages(requestID: flowModel.requestID) 77 | previewImageView?.startAnimating() 78 | }else { 79 | previewImageView?.image = image 80 | } 81 | previewImageView?.contentMode = .scaleAspectFit 82 | previewScrollView?.addSubview(previewImageView!) 83 | 84 | navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Copy", style: .plain, target: self, action: #selector(copyItemDidClick)) 85 | }else if text != nil { 86 | initTextView(with: text!) 87 | 88 | navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Copy", style: .plain, target: self, action: #selector(copyItemDidClick)) 89 | }else if mediaURL != nil { 90 | previewPlayerController = AVPlayerViewController() 91 | previewPlayerController?.player = AVPlayer(url: mediaURL!) 92 | addChild(previewPlayerController!) 93 | view.addSubview(previewPlayerController!.view) 94 | } 95 | } 96 | 97 | @objc func copyItemDidClick() { 98 | if text != nil { 99 | UIPasteboard.general.string = text 100 | }else if image != nil { 101 | UIPasteboard.general.image = image 102 | } 103 | } 104 | 105 | override func viewDidLayoutSubviews() { 106 | super.viewDidLayoutSubviews() 107 | 108 | previewTextView?.frame = view.bounds 109 | previewScrollView?.frame = view.bounds 110 | previewScrollView?.contentSize = CGSize(width: view.bounds.size.width, height: view.bounds.size.height) 111 | var imageWidth = previewImageView?.image?.size.width ?? 0 112 | var imageHeight = previewImageView?.image?.size.width ?? 0 113 | if previewImageView?.animationImages?.isEmpty == false { 114 | imageWidth = previewImageView?.animationImages?.first?.size.width ?? 0 115 | imageHeight = previewImageView?.animationImages?.first?.size.height ?? 0 116 | } 117 | let imageViewWidth = min(imageWidth, view.bounds.size.width) 118 | let imageViewHeight = min(imageHeight, view.bounds.size.height) 119 | previewImageView?.bounds = CGRect(x: 0, y: 0, width: imageViewWidth, height: imageViewHeight) 120 | previewImageView?.center = CGPoint(x: view.bounds.size.width/2, y: view.bounds.size.height/2) 121 | previewPlayerController?.view.frame = view.bounds 122 | } 123 | 124 | func initTextView(with text: String) { 125 | previewTextView = UITextView() 126 | previewTextView?.isEditable = false 127 | previewTextView?.font = .systemFont(ofSize: 12) 128 | previewTextView?.textColor = .black 129 | previewTextView?.backgroundColor = .white 130 | previewTextView?.isScrollEnabled = true 131 | previewTextView?.textAlignment = .left 132 | previewTextView?.text = text 133 | view.addSubview(previewTextView!) 134 | } 135 | 136 | //MARK: - UIScrollViewDelegate 137 | func viewForZooming(in scrollView: UIScrollView) -> UIView? { 138 | return previewImageView 139 | } 140 | 141 | func scrollViewDidZoom(_ scrollView: UIScrollView) { 142 | var center = CGPoint(x: view.bounds.size.width/2, y: view.bounds.size.height/2) 143 | if scrollView.contentSize.width > view.bounds.size.width { 144 | center.x = scrollView.contentSize.width/2 145 | } 146 | if scrollView.contentSize.height > view.bounds.size.height { 147 | center.y = scrollView.contentSize.height/2 148 | } 149 | previewImageView?.center = center 150 | } 151 | 152 | } 153 | -------------------------------------------------------------------------------- /Sources/UI/NetworkObserver/NetworkObserverDashboardViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkObserverDashboardViewController.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/29. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class NetworkObserverDashboardViewController: UITableViewController { 12 | let soldier: NetworkObserverSoldier 13 | 14 | init(soldier: NetworkObserverSoldier) { 15 | self.soldier = soldier 16 | super.init(style: .plain) 17 | } 18 | 19 | required init?(coder aDecoder: NSCoder) { 20 | fatalError("init(coder:) has not been implemented") 21 | } 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | title = "流量" 27 | tableView.register(DashboardCell.self, forCellReuseIdentifier: "swithCell") 28 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") 29 | tableView.tableFooterView = UIView() 30 | } 31 | 32 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 33 | return 3 34 | } 35 | 36 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 37 | if indexPath.row == 0 { 38 | let cell = tableView.dequeueReusableCell(withIdentifier: "swithCell", for: indexPath) as! DashboardCell 39 | cell.textLabel?.text = "流量检测开关" 40 | cell.toggle.isOn = soldier.isActive 41 | cell.toggleValueDidChange = {[weak self] (isOn) in 42 | if isOn { 43 | self?.soldier.start() 44 | let alert = UIAlertController(title: nil, message: "需要重新启动APP才能生效!", preferredStyle: .alert) 45 | alert.addAction(UIAlertAction(title: "确定", style: .cancel, handler: nil)) 46 | self?.present(alert, animated: true, completion: nil) 47 | }else { 48 | self?.soldier.end() 49 | } 50 | } 51 | return cell 52 | }else if indexPath.row == 1 { 53 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) 54 | cell.accessoryType = .disclosureIndicator 55 | cell.textLabel?.text = "查看接口请求列表" 56 | return cell 57 | }else { 58 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) 59 | cell.accessoryType = .disclosureIndicator 60 | cell.textLabel?.text = "清理接口请求记录" 61 | return cell 62 | } 63 | } 64 | 65 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 66 | tableView.deselectRow(at: indexPath, animated: true) 67 | if indexPath.row == 1 { 68 | let vc = NetworkFlowListViewController(soldier: soldier) 69 | self.navigationController?.pushViewController(vc, animated: true) 70 | }else if indexPath.row == 2 { 71 | let alert = UIAlertController(title: "提示", message: "确认删除所有接口请求记录吗?", preferredStyle: .alert) 72 | alert.addAction(UIAlertAction(title: "取消", style: .cancel, handler: nil)) 73 | alert.addAction(UIAlertAction(title: "确定", style: .destructive, handler: { (action) in 74 | NetworkObserverSoldier.shared.flowModels.removeAll() 75 | })) 76 | present(alert, animated: true, completion: nil) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/UI/SanboxBrowser/JXDatabaseConnector.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JXDatabaseConnector.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/9/20. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SQLite3 11 | 12 | public class JXDatabaseConnector { 13 | let databasePath: String 14 | var db: OpaquePointer? 15 | 16 | public init(path: String) { 17 | databasePath = path 18 | } 19 | 20 | @discardableResult 21 | public func open() -> Bool { 22 | if db != nil { 23 | return true 24 | } 25 | let resultCode = sqlite3_open(databasePath, &db) 26 | if resultCode == SQLITE_OK { 27 | return true 28 | } 29 | print("open \(databasePath) error:\(resultCode)") 30 | return false 31 | } 32 | 33 | @discardableResult 34 | func close() -> Bool { 35 | guard let db = db else { 36 | return true 37 | } 38 | var resultCode: Int32 = 0 39 | var retry = false 40 | var triedFinalizingOpenStatements = false 41 | repeat { 42 | retry = false 43 | resultCode = sqlite3_close(db) 44 | if SQLITE_BUSY == resultCode || SQLITE_LOCKED == resultCode { 45 | if !triedFinalizingOpenStatements { 46 | triedFinalizingOpenStatements = true 47 | var pStmt: OpaquePointer? 48 | pStmt = sqlite3_next_stmt(db, nil) 49 | while pStmt != nil { 50 | sqlite3_finalize(pStmt) 51 | retry = true 52 | pStmt = sqlite3_next_stmt(db, nil) 53 | } 54 | }else if SQLITE_OK != resultCode { 55 | print("close \(databasePath) error:\(resultCode)") 56 | } 57 | } 58 | } while retry 59 | self.db = nil 60 | return true 61 | } 62 | 63 | public func allTables() -> [String] { 64 | let queryResult = executeQuery(sql: "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name") 65 | let result = queryResult.compactMap { (dict) -> String? in 66 | return dict["name"] as? String 67 | } 68 | return result 69 | } 70 | 71 | public func allColumns(with tableName: String) -> [String] { 72 | let queryResult = executeQuery(sql: "PRAGMA table_info('\(tableName)')") 73 | let result = queryResult.compactMap { (dict) -> String? in 74 | return dict["name"] as? String 75 | } 76 | return result 77 | } 78 | 79 | public func allData(with tableName: String) -> [[String:Any]] { 80 | return executeQuery(sql: "SELECT * FROM \(tableName)") 81 | } 82 | 83 | func executeQuery(sql: String) -> [[String:Any]] { 84 | open() 85 | var resultArray = [[String:Any]]() 86 | var pstmt: OpaquePointer? 87 | if sqlite3_prepare_v2(db, sql, -1, &pstmt, nil) == SQLITE_OK, pstmt != nil { 88 | while sqlite3_step(pstmt) == SQLITE_ROW { 89 | let dataNumber = sqlite3_data_count(pstmt) 90 | guard dataNumber > 0 else { 91 | continue 92 | } 93 | var dict = [String:Any].init(minimumCapacity: Int(dataNumber)) 94 | let columnCount = sqlite3_column_count(pstmt) 95 | for index in 0.. Any? { 110 | let type = sqlite3_column_type(stmt, columnIndex) 111 | var resultValue: Any? 112 | if type == SQLITE_INTEGER { 113 | resultValue = sqlite3_column_int64(stmt, columnIndex) 114 | }else if type == SQLITE_FLOAT { 115 | resultValue = sqlite3_column_double(stmt, columnIndex) 116 | }else if type == SQLITE_BLOB { 117 | resultValue = dataForColumnIndex(columnIndex, stmt: stmt) 118 | }else { 119 | resultValue = stringForColumnIndex(columnIndex, stmt: stmt) 120 | } 121 | return resultValue 122 | } 123 | 124 | func dataForColumnIndex(_ columnIndex: Int32, stmt: OpaquePointer) -> Data? { 125 | if sqlite3_column_type(stmt, columnIndex) == SQLITE_NULL || columnIndex < 0 { 126 | return nil 127 | } 128 | let dataBuffer = sqlite3_column_blob(stmt, columnIndex) 129 | let dataSize = sqlite3_column_bytes(stmt, columnIndex) 130 | if dataBuffer == nil { 131 | return nil 132 | } 133 | return Data(bytes: dataBuffer!, count: Int(dataSize)) 134 | } 135 | 136 | func stringForColumnIndex(_ columnIndex: Int32, stmt: OpaquePointer) -> String? { 137 | if sqlite3_column_type(stmt, columnIndex) == SQLITE_NULL || columnIndex < 0 { 138 | return nil 139 | } 140 | let cString = sqlite3_column_text(stmt, columnIndex) 141 | if cString == nil { 142 | return nil 143 | } 144 | return String(cString: cString!) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /Sources/UI/SanboxBrowser/JXFileBrowserController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JXFileBrowserController.swift 3 | // JXFileBrowserController 4 | // 5 | // Created by jiaxin on 2018/7/6. 6 | // Copyright © 2018年 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class JXFileBrowserController: UIViewController { 12 | public let path: String! 13 | public var tableView: UITableView! 14 | public var dataSource: [String]! 15 | private let mainBundleResourcePath = "mainBundleResourcePath" 16 | private let emptyTips = "There are no files here." 17 | 18 | init(path: String) { 19 | self.path = path 20 | super.init(nibName: nil, bundle: nil) 21 | } 22 | 23 | required public init?(coder aDecoder: NSCoder) { 24 | fatalError("init(coder:) has not been implemented") 25 | } 26 | 27 | override open func viewDidLoad() { 28 | super.viewDidLoad() 29 | 30 | if path == NSHomeDirectory() { 31 | self.title = "NSHomeDirectory" 32 | }else { 33 | self.title = URL(fileURLWithPath: path).lastPathComponent 34 | } 35 | 36 | dataSource = [String]() 37 | if path == NSHomeDirectory(), Bundle.main.resourcePath != nil { 38 | dataSource.append(mainBundleResourcePath) 39 | } 40 | do { 41 | let fileNames = try FileManager.default.contentsOfDirectory(atPath: path) 42 | dataSource.append(contentsOf: fileNames) 43 | } catch let error { 44 | print("The error of contentsOfDirectory: %@", error) 45 | } 46 | 47 | tableView = UITableView(frame: self.view.bounds, style: .plain) 48 | tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 49 | tableView.dataSource = self 50 | tableView.delegate = self 51 | tableView.tableFooterView = UIView() 52 | view.addSubview(tableView) 53 | 54 | if dataSource.isEmpty { 55 | let emptyLabel = UILabel() 56 | emptyLabel.font = .systemFont(ofSize: 25) 57 | emptyLabel.text = emptyTips 58 | emptyLabel.textAlignment = .center 59 | tableView.backgroundView = emptyLabel 60 | } 61 | } 62 | 63 | func isImagePathExtension(filePath: String) -> Bool { 64 | return ["jpg", "jpeg", "png", "gif", "tiff", "tif"].contains(URL(fileURLWithPath: filePath).pathExtension) 65 | } 66 | } 67 | 68 | extension JXFileBrowserController: UITableViewDataSource, UITableViewDelegate { 69 | public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 70 | return dataSource.count 71 | } 72 | 73 | public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 74 | var cell = tableView.dequeueReusableCell(withIdentifier: "cell") 75 | if cell == nil { 76 | cell = UITableViewCell(style: .subtitle, reuseIdentifier: "cell") 77 | } 78 | cell?.accessoryType = .disclosureIndicator 79 | cell?.detailTextLabel?.textColor = UIColor.lightGray 80 | let source = dataSource[indexPath.row] 81 | cell?.textLabel?.text = source 82 | var fullPath = path + "/" + source 83 | if source == mainBundleResourcePath { 84 | fullPath = Bundle.main.resourcePath! 85 | } 86 | do { 87 | let attributes = try FileManager.default.attributesOfItem(atPath: fullPath) 88 | if attributes[FileAttributeKey.type] as! String == FileAttributeType.typeDirectory.rawValue { 89 | let count = (try? FileManager.default.contentsOfDirectory(atPath: fullPath).count) ?? 0 90 | cell?.detailTextLabel?.text = String(format: "%d file%@", count, ((count > 1) ? "s" : "")) 91 | }else { 92 | let fileSize = attributes[FileAttributeKey.size] as! Double 93 | let fileSizeString = ByteCountFormatter.string(fromByteCount: Int64(fileSize), countStyle: ByteCountFormatter.CountStyle.file) 94 | cell?.detailTextLabel?.text = fileSizeString 95 | 96 | if isImagePathExtension(filePath: fullPath) { 97 | cell?.imageView?.contentMode = .scaleAspectFit 98 | cell?.imageView?.clipsToBounds = true 99 | cell?.imageView?.image = UIImage(contentsOfFile: fullPath) 100 | }else { 101 | cell?.imageView?.image = nil 102 | } 103 | } 104 | } catch let error { 105 | print("The error of attributesOfItem: %@", error) 106 | } 107 | return cell! 108 | } 109 | 110 | public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 111 | tableView.deselectRow(at: indexPath, animated: true) 112 | let source = dataSource[indexPath.row] 113 | var fullPath = path + "/" + source 114 | if source == mainBundleResourcePath { 115 | fullPath = Bundle.main.resourcePath! 116 | } 117 | do { 118 | let attributes = try FileManager.default.attributesOfItem(atPath: fullPath) 119 | if attributes[FileAttributeKey.type] as? String == FileAttributeType.typeDirectory.rawValue { 120 | let fileBrowserController = JXFileBrowserController(path: fullPath) 121 | self.navigationController?.pushViewController(fileBrowserController, animated: true) 122 | }else { 123 | let fileExtension = URL(fileURLWithPath: fullPath).pathExtension.lowercased() 124 | if JXTableListViewController.supportsExtension(fileExtension) { 125 | let vc = JXTableListViewController(filePath: fullPath) 126 | navigationController?.pushViewController(vc, animated: true) 127 | }else if JXFilePreviewViewController.supportsExtension(fileExtension) { 128 | let previewVC = JXFilePreviewViewController(filePath: fullPath) 129 | self.navigationController?.pushViewController(previewVC, animated: true) 130 | }else { 131 | let sheet = UIAlertController(title: nil, message: "Unsupport this file, you can share it.", preferredStyle: .actionSheet) 132 | sheet.addAction(UIAlertAction(title: "Share", style: .default, handler: { (action) in 133 | let activityController = UIActivityViewController(activityItems: [URL(fileURLWithPath: fullPath)], applicationActivities: nil) 134 | self.present(activityController, animated: true, completion: nil) 135 | })) 136 | sheet.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) 137 | present(sheet, animated: true, completion: nil) 138 | } 139 | } 140 | } catch let error { 141 | let alert = UIAlertController(title: "The error of 'FileManager.default.attributesOfItem'", message: error.localizedDescription, preferredStyle: .alert) 142 | let confirm = UIAlertAction(title: "I know", style: .cancel, handler: nil) 143 | alert.addAction(confirm) 144 | self.present(alert, animated: true, completion: nil) 145 | print("The error of attributesOfItem: %@", error) 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Sources/UI/SanboxBrowser/JXFilePreviewViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JXFilePreviewViewController.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/23. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AVFoundation 11 | import AVKit 12 | 13 | class JXFilePreviewViewController: UIViewController, UIScrollViewDelegate { 14 | let filePath: String 15 | var previewImageView: UIImageView? 16 | var previewScrollView: UIScrollView? 17 | var previewTextView: UITextView? 18 | var previewPlayerController: AVPlayerViewController? 19 | 20 | init(filePath: String) { 21 | self.filePath = filePath 22 | super.init(nibName: nil, bundle: nil) 23 | } 24 | 25 | required init?(coder aDecoder: NSCoder) { 26 | fatalError("init(coder:) has not been implemented") 27 | } 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | 32 | title = "File Preview" 33 | view.backgroundColor = .white 34 | 35 | let fileExtension = URL(fileURLWithPath: filePath).pathExtension.lowercased() 36 | if ["png", "jpg", "jpeg", "gif"].contains(fileExtension) { 37 | let image = UIImage(contentsOfFile: filePath) 38 | previewScrollView = UIScrollView() 39 | previewScrollView?.delegate = self 40 | previewScrollView?.minimumZoomScale = 1 41 | var imageWidthScale: CGFloat = 1 42 | if view.bounds.size.width > 0 { 43 | imageWidthScale = (image?.size.width ?? 0)/view.bounds.size.width 44 | } 45 | previewScrollView?.maximumZoomScale = max(2, imageWidthScale) 46 | view.addSubview(previewScrollView!) 47 | 48 | previewImageView = UIImageView() 49 | if fileExtension == "gif" { 50 | let url = URL(fileURLWithPath: filePath) 51 | if let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil) { 52 | let imageCount = CGImageSourceGetCount(imageSource) 53 | var images = [UIImage]() 54 | for index in 0.. String in 73 | return "\(result)\n\(item.key):\(item.value)" 74 | } 75 | initTextView(with: text) 76 | }else { 77 | initTextView(with: "该文件没有内容") 78 | } 79 | }else if "strings" == fileExtension { 80 | if let data = FileManager.default.contents(atPath: filePath), let keyValues = try? PropertyListSerialization.propertyList(from: data, options: .mutableContainersAndLeaves, format: nil) as? [String] { 81 | let text = keyValues.reduce("") { (result, item) -> String in 82 | return "\(result)\n\(item)" 83 | } 84 | initTextView(with: text) 85 | }else { 86 | initTextView(with: "该文件没有内容") 87 | } 88 | }else { 89 | if let data = FileManager.default.contents(atPath: filePath), let text = String.init(data: data, encoding: .utf8) { 90 | initTextView(with: text) 91 | }else { 92 | initTextView(with: "该文件没有内容") 93 | } 94 | } 95 | }else if ["mp4", "mov", "3gp", "m4v", "avi", "aac", "mp3", "m4a", "flac", "wav", "ac3", "aa", "aax"].contains(fileExtension) { 96 | let player = AVPlayer(url: URL(fileURLWithPath: filePath)) 97 | previewPlayerController = AVPlayerViewController() 98 | previewPlayerController?.player = player 99 | addChild(previewPlayerController!) 100 | view.addSubview(previewPlayerController!.view) 101 | }else { 102 | initTextView(with: "不支持该文件类型\(URL(fileURLWithPath: filePath).pathExtension)") 103 | } 104 | 105 | navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Share", style: .plain, target: self, action: #selector(didNaviShareItemClick)) 106 | } 107 | 108 | @objc func didNaviShareItemClick() { 109 | let activityController = UIActivityViewController(activityItems: [URL(fileURLWithPath: filePath)], applicationActivities: nil) 110 | self.present(activityController, animated: true, completion: nil) 111 | } 112 | 113 | class func supportsExtension(_ extension: String) -> Bool { 114 | let extensions = ["png", "jpg", "jpeg", "gif", "strings", "plist", "txt", "log", "csv", "mp4", "mov", "3gp", "m4v", "avi", "aac", "mp3", "m4a", "flac", "wav", "ac3", "aa", "aax"] 115 | return extensions.contains(`extension`) 116 | } 117 | 118 | override func viewDidDisappear(_ animated: Bool) { 119 | super.viewDidDisappear(animated) 120 | 121 | previewPlayerController?.player?.pause() 122 | } 123 | 124 | override func viewDidLayoutSubviews() { 125 | super.viewDidLayoutSubviews() 126 | 127 | previewTextView?.frame = view.bounds 128 | previewScrollView?.frame = view.bounds 129 | previewScrollView?.contentSize = CGSize(width: view.bounds.size.width, height: view.bounds.size.height) 130 | var imageWidth = previewImageView?.image?.size.width ?? 0 131 | var imageHeight = previewImageView?.image?.size.width ?? 0 132 | if previewImageView?.animationImages?.isEmpty == false { 133 | imageWidth = previewImageView?.animationImages?.first?.size.width ?? 0 134 | imageHeight = previewImageView?.animationImages?.first?.size.height ?? 0 135 | } 136 | let imageViewWidth = min(imageWidth, view.bounds.size.width) 137 | let imageViewHeight = min(imageHeight, view.bounds.size.height) 138 | previewImageView?.bounds = CGRect(x: 0, y: 0, width: imageViewWidth, height: imageViewHeight) 139 | previewImageView?.center = CGPoint(x: view.bounds.size.width/2, y: view.bounds.size.height/2) 140 | previewPlayerController?.view.frame = view.bounds 141 | } 142 | 143 | func initTextView(with text: String) { 144 | previewTextView = UITextView() 145 | previewTextView?.isEditable = false 146 | previewTextView?.font = .systemFont(ofSize: 12) 147 | previewTextView?.textColor = .black 148 | previewTextView?.backgroundColor = .white 149 | previewTextView?.isScrollEnabled = true 150 | previewTextView?.textAlignment = .left 151 | previewTextView?.text = text 152 | view.addSubview(previewTextView!) 153 | } 154 | 155 | //MARK: - UIScrollViewDelegate 156 | func viewForZooming(in scrollView: UIScrollView) -> UIView? { 157 | return previewImageView 158 | } 159 | 160 | func scrollViewDidZoom(_ scrollView: UIScrollView) { 161 | var center = CGPoint(x: view.bounds.size.width/2, y: view.bounds.size.height/2) 162 | if scrollView.contentSize.width > view.bounds.size.width { 163 | center.x = scrollView.contentSize.width/2 164 | } 165 | if scrollView.contentSize.height > view.bounds.size.height { 166 | center.y = scrollView.contentSize.height/2 167 | } 168 | previewImageView?.center = center 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /Sources/UI/SanboxBrowser/JXTableContentViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JXTableContentViewController.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/9/20. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class JXTableContentViewController: UIViewController { 12 | let filePath: String 13 | let tableName: String 14 | let excelView: ExcelView 15 | let connector: JXDatabaseConnector 16 | let allColumns: [String] 17 | let allDatabaseData: [[String:Any]] 18 | let allRowStrings: [[String]] 19 | 20 | init(filePath: String, tableName: String) { 21 | self.filePath = filePath 22 | self.tableName = tableName 23 | connector = JXDatabaseConnector(path: filePath) 24 | allColumns = connector.allColumns(with: tableName) 25 | allDatabaseData = connector.allData(with: tableName) 26 | var tempAllRowStrings = [[String]]() 27 | for row in 0.. Int { 82 | return allDatabaseData.count 83 | } 84 | 85 | func numberOfColumns(in excelView: ExcelView) -> Int { 86 | return allColumns.count 87 | } 88 | 89 | func excelView(_ excelView: ExcelView, rowNameAt row: Int) -> String { 90 | return "\(row)" 91 | } 92 | 93 | func excelView(_ excelView: ExcelView, columnNameAt column: Int) -> String { 94 | return allColumns[column] 95 | } 96 | 97 | func excelView(_ excelView: ExcelView, rowDatasAt row: Int) -> [String] { 98 | return allRowStrings[row] 99 | } 100 | 101 | func excelView(_ excelView: ExcelView, rowHeightAt row: Int) -> CGFloat { 102 | return 40 103 | } 104 | 105 | func excelView(_ excelView: ExcelView, columnWidthAt column: Int) -> CGFloat { 106 | return 120 107 | } 108 | 109 | func widthOfLeftHeader(in excelView: ExcelView) -> CGFloat { 110 | return 40 111 | } 112 | 113 | func heightOfTopHeader(in excelView: ExcelView) -> CGFloat { 114 | return 40 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Sources/UI/SanboxBrowser/JXTableListViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JXTableListViewController.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/9/20. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class JXTableListViewController: UITableViewController { 12 | let filePath: String 13 | let tableNames: [String] 14 | 15 | init(filePath: String) { 16 | self.filePath = filePath 17 | let connector = JXDatabaseConnector(path: filePath) 18 | tableNames = connector.allTables() 19 | super.init(nibName: nil, bundle: nil) 20 | } 21 | 22 | required init?(coder aDecoder: NSCoder) { 23 | fatalError("init(coder:) has not been implemented") 24 | } 25 | 26 | override func viewDidLoad() { 27 | super.viewDidLoad() 28 | 29 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") 30 | navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Share", style: .plain, target: self, action: #selector(didNaviShareItemClick)) 31 | } 32 | 33 | class func supportsExtension(_ extension: String) -> Bool { 34 | let extensions = ["db", "sqlite", "sqlite3"] 35 | return extensions.contains(`extension`) 36 | } 37 | 38 | @objc func didNaviShareItemClick() { 39 | let activityController = UIActivityViewController(activityItems: [URL(fileURLWithPath: filePath)], applicationActivities: nil) 40 | self.present(activityController, animated: true, completion: nil) 41 | } 42 | 43 | //MARK: - UITableViewDataSource & UITableViewDelegate 44 | 45 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 46 | return tableNames.count 47 | } 48 | 49 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 50 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) 51 | cell.textLabel?.text = tableNames[indexPath.row] 52 | return cell 53 | } 54 | 55 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 56 | let vc = JXTableContentViewController(filePath: filePath, tableName: tableNames[indexPath.row]) 57 | navigationController?.pushViewController(vc, animated: true) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/UI/SanboxBrowser/JXTextPreviewViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JXTextPreviewViewController.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/10/8. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class JXTextPreviewViewController: UIViewController { 12 | let text: String 13 | var textView: UITextView! 14 | 15 | init(text: String) { 16 | self.text = text 17 | super.init(nibName: nil, bundle: nil) 18 | } 19 | 20 | required init?(coder: NSCoder) { 21 | fatalError("init(coder:) has not been implemented") 22 | } 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | 27 | view.backgroundColor = .white 28 | 29 | textView = UITextView() 30 | textView.isEditable = false 31 | textView.font = .systemFont(ofSize: 12) 32 | textView.textColor = .black 33 | textView.backgroundColor = .white 34 | textView.isScrollEnabled = true 35 | textView.textAlignment = .left 36 | textView.text = text 37 | view.addSubview(textView) 38 | 39 | navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Copy", style: .plain, target: self, action: #selector(didNaviCopyItemClick)) 40 | } 41 | 42 | @objc func didNaviCopyItemClick() { 43 | UIPasteboard.general.string = text 44 | } 45 | 46 | override func viewDidLayoutSubviews() { 47 | super.viewDidLayoutSubviews() 48 | 49 | let margin: CGFloat = 12 50 | textView.frame = view.bounds.inset(by: UIEdgeInsets(top: margin, left: margin, bottom: margin, right: margin)) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/UI/UserDefaults/UserDefaultsKeyValueDetailViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultsKeyValueDetailViewController.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/9/9. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class UserDefaultsKeyValueDetailViewController: BaseViewController { 12 | let defaults: UserDefaults 13 | var keyValueModel: UserDefaultsKeyValueModel 14 | var tipsLabel: UILabel! 15 | var inputTextView: UITextView! 16 | 17 | init(defaults: UserDefaults, keyValueModel: UserDefaultsKeyValueModel) { 18 | self.defaults = defaults 19 | self.keyValueModel = keyValueModel 20 | super.init(nibName: nil, bundle: nil) 21 | } 22 | 23 | required init?(coder aDecoder: NSCoder) { 24 | fatalError("init(coder:) has not been implemented") 25 | } 26 | 27 | override func viewDidLoad() { 28 | super.viewDidLoad() 29 | 30 | title = "Edit Value" 31 | 32 | let copy = UIBarButtonItem(title: "Copy", style: .plain, target: self, action: #selector(copyValue)) 33 | let update = UIBarButtonItem(title: "Update", style: .plain, target: self, action: #selector(updateValue)) 34 | 35 | tipsLabel = UILabel() 36 | tipsLabel.text = "Key:\(keyValueModel.key)" 37 | view.addSubview(tipsLabel) 38 | 39 | inputTextView = UITextView() 40 | inputTextView.font = .systemFont(ofSize: 17) 41 | inputTextView.keyboardType = .URL 42 | inputTextView.layer.borderColor = UIColor.lightGray.cgColor 43 | inputTextView.layer.borderWidth = 1 44 | inputTextView.text = keyValueModel.valueDescription() 45 | view.addSubview(inputTextView) 46 | 47 | inputTextView.isEditable = true 48 | navigationItem.rightBarButtonItems = [update, copy] 49 | switch keyValueModel.value { 50 | case .string(_): 51 | inputTextView.keyboardType = .default 52 | case .number(_): 53 | inputTextView.keyboardType = .numbersAndPunctuation 54 | default: 55 | inputTextView.isEditable = false 56 | navigationItem.rightBarButtonItems = [copy] 57 | } 58 | } 59 | 60 | override func viewDidLayoutSubviews() { 61 | super.viewDidLayoutSubviews() 62 | 63 | tipsLabel.frame = CGRect(x: 12, y: 12, width: view.bounds.size.width - 12*2, height: 20) 64 | inputTextView.frame = CGRect(x: 12, y: tipsLabel.frame.maxY + 3, width: view.bounds.size.width - 12*2, height: 200) 65 | } 66 | 67 | override func touchesBegan(_ touches: Set, with event: UIEvent?) { 68 | inputTextView.endEditing(true) 69 | } 70 | 71 | @objc func copyValue() { 72 | UIPasteboard.general.string = keyValueModel.valueDescription() 73 | } 74 | 75 | @objc func updateValue() { 76 | if inputTextView.text.isEmpty { 77 | defaults.setValue(nil, forKey: keyValueModel.key) 78 | return 79 | } 80 | guard let newValueText = inputTextView.text else { 81 | return 82 | } 83 | defaults.set(newValueText, forKey: keyValueModel.key) 84 | if let newValue = defaults.value(forKey: keyValueModel.key) { 85 | keyValueModel = UserDefaultsKeyValueModel(key: keyValueModel.key, value: newValue, userDefaults: defaults) 86 | inputTextView.text = keyValueModel.valueDescription() 87 | }else { 88 | inputTextView.text = nil 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Sources/UI/UserDefaults/UserDefaultsKeyValuesListViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultsKeyValuesListViewController.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/9/9. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class UserDefaultsKeyValuesListViewController: BaseTableViewController { 12 | let defaults: UserDefaults 13 | var dataSource = [UserDefaultsKeyValueModel]() 14 | var filteredDataSource = [UserDefaultsKeyValueModel]() 15 | var searchController: UISearchController! 16 | 17 | init(defaults: UserDefaults) { 18 | self.defaults = defaults 19 | 20 | var tempDataSource = [UserDefaultsKeyValueModel]() 21 | defaults.dictionaryRepresentation().forEach { (keyValue) in 22 | let model = UserDefaultsKeyValueModel(key: keyValue.key, value: keyValue.value, userDefaults: defaults) 23 | tempDataSource.append(model) 24 | } 25 | tempDataSource.sort { (model1, model2) -> Bool in 26 | return model1.key < model2.key 27 | } 28 | dataSource = tempDataSource 29 | super.init(style: .plain) 30 | } 31 | 32 | required init?(coder aDecoder: NSCoder) { 33 | fatalError("init(coder:) has not been implemented") 34 | } 35 | 36 | override func viewDidLoad() { 37 | super.viewDidLoad() 38 | 39 | title = "Key Value List" 40 | 41 | searchController = UISearchController(searchResultsController: nil) 42 | searchController.searchBar.placeholder = "key filter" 43 | searchController.hidesNavigationBarDuringPresentation = false 44 | searchController.searchResultsUpdater = self 45 | searchController.dimsBackgroundDuringPresentation = false 46 | tableView.tableHeaderView = searchController.searchBar 47 | tableView.register(UserDefaultsKeyValuesListCell.self, forCellReuseIdentifier: "cell") 48 | 49 | NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChangeNotification(_:)), name: UserDefaults.didChangeNotification, object: nil) 50 | } 51 | 52 | func refreshDataSource() { 53 | var tempDataSource = [UserDefaultsKeyValueModel]() 54 | defaults.dictionaryRepresentation().forEach { (keyValue) in 55 | let model = UserDefaultsKeyValueModel(key: keyValue.key, value: keyValue.value, userDefaults: defaults) 56 | tempDataSource.append(model) 57 | } 58 | tempDataSource.sort { (model1, model2) -> Bool in 59 | return model1.key < model2.key 60 | } 61 | dataSource = tempDataSource 62 | } 63 | 64 | @objc func userDefaultsDidChangeNotification(_ noti: Notification) { 65 | DispatchQueue.main.async { 66 | self.refreshDataSource() 67 | self.tableView.reloadData() 68 | } 69 | } 70 | 71 | //MARK: - UITableViewDataSource & UITableViewDelegate 72 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 73 | if searchController.isActive { 74 | return filteredDataSource.count 75 | } 76 | return dataSource.count 77 | } 78 | 79 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 80 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) 81 | var model: UserDefaultsKeyValueModel! 82 | if searchController.isActive { 83 | model = filteredDataSource[indexPath.row] 84 | }else { 85 | model = dataSource[indexPath.row] 86 | } 87 | cell.textLabel?.text = model.key 88 | cell.detailTextLabel?.text = model.valueDescription() 89 | cell.accessoryType = .disclosureIndicator 90 | return cell 91 | } 92 | 93 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 94 | var model: UserDefaultsKeyValueModel! 95 | if searchController.isActive { 96 | model = filteredDataSource[indexPath.row] 97 | }else { 98 | model = dataSource[indexPath.row] 99 | } 100 | searchController.isActive = false 101 | let vc = UserDefaultsKeyValueDetailViewController(defaults: defaults, keyValueModel: model) 102 | navigationController?.pushViewController(vc, animated: true) 103 | } 104 | } 105 | 106 | extension UserDefaultsKeyValuesListViewController: UISearchResultsUpdating { 107 | func updateSearchResults(for searchController: UISearchController) { 108 | if searchController.searchBar.text?.isEmpty == false { 109 | filteredDataSource.removeAll() 110 | for model in dataSource { 111 | if model.key.range(of: searchController.searchBar.text!, options: .caseInsensitive) != nil { 112 | filteredDataSource.append(model) 113 | } 114 | } 115 | }else { 116 | filteredDataSource = dataSource 117 | } 118 | tableView.reloadData() 119 | } 120 | } 121 | 122 | class UserDefaultsKeyValuesListCell: UITableViewCell { 123 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 124 | super.init(style: .subtitle, reuseIdentifier: reuseIdentifier) 125 | } 126 | 127 | required init?(coder aDecoder: NSCoder) { 128 | fatalError("init(coder:) has not been implemented") 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Sources/UI/WebsiteEntry/WebDetailViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebDetailViewController.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/23. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import WebKit 11 | 12 | class WebDetailViewController: BaseViewController, WKNavigationDelegate { 13 | let website: String 14 | let webView: WKWebView 15 | let progressLine: CALayer 16 | 17 | deinit { 18 | webView.removeObserver(self, forKeyPath: "estimatedProgress") 19 | } 20 | 21 | init(website: String) { 22 | if website.hasPrefix("http://") || website.hasPrefix("https://") { 23 | self.website = website 24 | }else { 25 | self.website = "https://" + website 26 | } 27 | webView = WKWebView(frame: CGRect.zero) 28 | progressLine = CALayer() 29 | super.init(nibName: nil, bundle: nil) 30 | } 31 | 32 | required init?(coder aDecoder: NSCoder) { 33 | fatalError("init(coder:) has not been implemented") 34 | } 35 | 36 | override func viewDidLoad() { 37 | super.viewDidLoad() 38 | 39 | webView.navigationDelegate = self 40 | webView.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil) 41 | webView.allowsBackForwardNavigationGestures = true 42 | view.addSubview(webView) 43 | 44 | progressLine.backgroundColor = UIColor.blue.withAlphaComponent(0.5).cgColor 45 | view.layer.addSublayer(progressLine) 46 | 47 | guard let url = URL(string: website) else { 48 | let alert = UIAlertController(title: nil, message: "网址无效!", preferredStyle: .alert) 49 | alert.addAction(UIAlertAction(title: "确定", style: .cancel, handler: { (action) in 50 | self.navigationController?.popViewController(animated: true) 51 | })) 52 | present(alert, animated: true, completion: nil) 53 | return 54 | } 55 | let request = URLRequest(url: url) 56 | webView.load(request) 57 | } 58 | 59 | override func viewDidLayoutSubviews() { 60 | webView.frame = view.bounds 61 | } 62 | 63 | override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { 64 | if keyPath == "estimatedProgress" { 65 | if webView.estimatedProgress == 0 || webView.estimatedProgress == 1 { 66 | progressLine.frame = CGRect.zero 67 | progressLine.isHidden = true 68 | }else { 69 | progressLine.isHidden = false 70 | } 71 | progressLine.frame = CGRect(x: 0, y: 0, width: CGFloat(webView.estimatedProgress)*view.bounds.size.width, height: 5) 72 | }else { 73 | super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) 74 | } 75 | } 76 | 77 | //MARK: - WKNavigationDelegate 78 | func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { 79 | print("JXCaptain H5任意门 didFail with error:\(error.localizedDescription)") 80 | let alert = UIAlertController(title: nil, message: error.localizedDescription, preferredStyle: .alert) 81 | alert.addAction(UIAlertAction(title: "确定", style: .cancel, handler: nil)) 82 | present(alert, animated: true, completion: nil) 83 | } 84 | 85 | func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { 86 | webView.evaluateJavaScript("document.title") { (result, error) in 87 | if let title = result as? String { 88 | self.title = title 89 | } 90 | } 91 | } 92 | 93 | func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { 94 | let alert = UIAlertController(title: nil, message: error.localizedDescription, preferredStyle: .alert) 95 | alert.addAction(UIAlertAction(title: "确定", style: .cancel, handler: nil)) 96 | present(alert, animated: true, completion: nil) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sources/UI/WebsiteEntry/WebsiteEntryViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebsiteEntryViewController.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/8/23. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class WebsiteEntryViewController: BaseViewController { 12 | let soldier: WebsiteEntrySoldier 13 | var tipsLabel: UILabel! 14 | var inputTextView: UITextView! 15 | var confirmButton: UIButton! 16 | 17 | init(soldier: WebsiteEntrySoldier) { 18 | self.soldier = soldier 19 | super.init(nibName: nil, bundle: nil) 20 | } 21 | 22 | required init?(coder aDecoder: NSCoder) { 23 | fatalError("init(coder:) has not been implemented") 24 | } 25 | 26 | override func viewDidLoad() { 27 | super.viewDidLoad() 28 | 29 | title = "H5任意门" 30 | 31 | tipsLabel = UILabel() 32 | tipsLabel.textColor = .lightGray 33 | tipsLabel.text = "请在下方输入网址:" 34 | view.addSubview(tipsLabel) 35 | 36 | inputTextView = UITextView() 37 | if soldier.defaultWebsite != nil { 38 | inputTextView.text = soldier.defaultWebsite 39 | }else { 40 | inputTextView.text = "https://" 41 | } 42 | inputTextView.font = .systemFont(ofSize: 17) 43 | inputTextView.keyboardType = .URL 44 | inputTextView.layer.borderColor = UIColor.lightGray.cgColor 45 | inputTextView.layer.borderWidth = 1 46 | view.addSubview(inputTextView) 47 | 48 | confirmButton = UIButton(type: .custom) 49 | confirmButton.setTitleColor(.black, for: .normal) 50 | confirmButton.setTitle("点击跳转", for: .normal) 51 | confirmButton.layer.cornerRadius = 5 52 | confirmButton.backgroundColor = UIColor.lightGray.withAlphaComponent(0.3) 53 | confirmButton.addTarget(self, action: #selector(confirmButtonDidClick), for: .touchUpInside) 54 | view.addSubview(confirmButton) 55 | } 56 | 57 | override func viewDidAppear(_ animated: Bool) { 58 | super.viewDidAppear(animated) 59 | 60 | inputTextView.becomeFirstResponder() 61 | } 62 | 63 | override func viewDidLayoutSubviews() { 64 | super.viewDidLayoutSubviews() 65 | 66 | tipsLabel.frame = CGRect(x: 12, y: 12, width: 200, height: 20) 67 | inputTextView.frame = CGRect(x: 12, y: tipsLabel.frame.maxY + 3, width: view.bounds.size.width - 12*2, height: 200) 68 | confirmButton.frame = CGRect(x: 12, y: inputTextView.frame.maxY + 10, width: inputTextView.bounds.size.width, height: 50) 69 | } 70 | 71 | override func touchesBegan(_ touches: Set, with event: UIEvent?) { 72 | inputTextView.endEditing(true) 73 | } 74 | 75 | @objc func confirmButtonDidClick() { 76 | guard !inputTextView.text.isEmpty else { 77 | let alert = UIAlertController(title: nil, message: "网址不能为空!", preferredStyle: .alert) 78 | alert.addAction(UIAlertAction(title: "确定", style: .cancel, handler: nil)) 79 | present(alert, animated: true, completion: nil) 80 | return 81 | } 82 | inputTextView.endEditing(true) 83 | if soldier.webDetailControllerClosure == nil { 84 | navigationController?.pushViewController(WebDetailViewController(website: inputTextView.text), animated: true) 85 | }else { 86 | navigationController?.pushViewController(soldier.webDetailControllerClosure!(inputTextView.text), animated: true) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Sources/UserDefaults+Access.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaults+Access.swift 3 | // JXCaptain 4 | // 5 | // Created by jiaxin on 2019/12/13. 6 | // Copyright © 2019 jiaxin. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension UserDefaults { 12 | var isNetworkObserverSoldierActive: Bool { 13 | set { set(newValue, forKey: #function) } 14 | get { bool(forKey: #function) } 15 | } 16 | var isANRSoldierActive: Bool { 17 | set { set(newValue, forKey: #function) } 18 | get { bool(forKey: #function) } 19 | } 20 | var isANRSoldierHasNewEvent: Bool { 21 | set { set(newValue, forKey: #function) } 22 | get { bool(forKey: #function) } 23 | } 24 | var isCPUSoldierActive: Bool { 25 | set { set(newValue, forKey: #function) } 26 | get { bool(forKey: #function) } 27 | } 28 | var isMemorySoldierActive: Bool { 29 | set { set(newValue, forKey: #function) } 30 | get { bool(forKey: #function) } 31 | } 32 | var isFPSSoldierActive: Bool { 33 | set { set(newValue, forKey: #function) } 34 | get { bool(forKey: #function) } 35 | } 36 | var isCrashSoldierHasNewEvent: Bool { 37 | set { set(newValue, forKey: #function) } 38 | get { bool(forKey: #function) } 39 | } 40 | } 41 | --------------------------------------------------------------------------------