App瘦身

作者: o_O小薯条 | 来源:发表于2018-09-03 15:06 被阅读0次

    写文章注册登录

    ×

    使用Swift3开发了个MacOS的程序可以检测出objc项目中无用方法,然后一键全部清理

     

    星光社的戴铭 

     关注

    2016.10.28 21:27* 字数 1901 阅读 1633评论 8喜欢 29

    当项目越来越大,引入第三方库越来越多,上架的 APP 体积也会越来越大,对于用户来说体验必定是不好的。在清理资源,编译选项优化,清理无用类等完成后,能够做而且效果会比较明显的就只有清理无用函数了。

    一种方案是我们滴滴的王康基于clang插件这样一个源码级别的分析工具来分析代码间的调用关系达到分析出无用代码的目的,文章在这里: 基于clang插件的一种iOS包大小瘦身方案 文章里对objc方法的定义,调用,实现的全面说明达到了极致,非常值得一看。

    另一种方案是根据 Linkmap 文件取到objc的所有类方法和实例方法。再用工具比如 otool 命令逆向出可执行文件里引用到的方法名然后通过求差集得到无用函数,由于API的回调也会被认为是无用函数,所以这个方案还需要将这些回调函数加到白名单里过滤。具体说明,可以看看微信团队的这篇文章: iOS微信安装包瘦身

    还有一种使用了 * machoview * 从 Mach-O 里获取信息进行无用方法和文件的处理。阿里有篇文章对 Mach-O 的处理做了详细的说明: 减小ipa体积之删除frameWork中无用mach-O文件

    这几个现有方案有些比较麻烦的地方,因为检索出的无用方法没法确定能够直接删除,还需要挨个检索人工判断是否可以删除,这样每次要清理时都需要这样人工排查一遍是非常耗时耗力的。

    这样就只有模拟编译过程对代码进行深入分析才能够找出确定能够删除的方法。具体效果可以先试试看,程序代码在:https://github.com/ming1016/SMCheckProject 选择工程目录后程序就开始检索无用方法然后将其注释掉。

    设置结构体 😚

    首先确定结构,类似先把 OC 文件根据语法画出整体结构。先看看 OC Runtime 里是如何设计的结构体。

    structobjc_object{Class isa  OBJC_ISA_AVAILABILITY;};/*类*/structobjc_class{Class isa  OBJC_ISA_AVAILABILITY;#if!__OBJC2__Class super_class;constchar*name;longversion;longinfo;longinstance_size;structobjc_ivar_list*ivars;structobjc_method_list**methodLists;structobjc_cache*cache;structobjc_protocol_list*protocols;#endif};/*成员变量列表*/structobjc_ivar_list{intivar_count#ifdef__LP64__intspace#endif/* variable length structure */structobjc_ivarivar_list[1]}      /*成员变量结构体*/structobjc_ivar{char*ivar_namechar*ivar_typeintivar_offset#ifdef__LP64__intspace#endif}/*方法列表*/structobjc_method_list{structobjc_method_list*obsolete;intmethod_count;#ifdef__LP64__intspace;#endif/* variable length structure */structobjc_methodmethod_list[1];};/*方法结构体*/structobjc_method{SEL method_name;char*method_types;/* a string representing argument/return types */IMP method_imp;};

    一个 class 只有少量函数会被调用,为了减少较大的遍历所以创建一个 objc_cache ,在找到一个方法后将 method_name 作为 key,将 method_imp 做值,再次发起时就可以直接在 cache 里找。

    使用 swift 创建类似的结构体,做些修改

    //文件classFile:NSObject{//文件publicvartype =FileType.FileHpublicvarname =""publicvarcontent =""publicvarmethods = [Method]()//所有方法publicvarimports = [Import]()//引入类}//引入structImport{publicvarfileName =""}//对象classObject{publicvarname =""publicvarsuperObject =""publicvarproperties = [Property]()publicvarmethods = [Method]()}//成员变量structProperty{publicvarname =""publicvartype =""}structMethod{publicvarclassMethodTf =false//+ or -publicvarreturnType =""publicvarreturnTypePointTf =falsepublicvarreturnTypeBlockTf =falsepublicvarparams = [MethodParam]()publicvarusedMethod = [Method]()publicvarfilePath =""//定义方法的文件路径,方便修改文件使用publicvarpnameId =""//唯一标识,便于快速比较}classMethodParam:NSObject{publicvarname =""publicvartype =""publicvartypePointTf =falsepublicvariName =""}classType:NSObject{//todo:更多类型publicvarname =""publicvartype =0//0是值类型 1是指针}```swift## 开始语法解析 😈首先遍历目录下所有的文件。```swiftletfileFolderPath =self.selectFolder()letfileFolderStringPath = fileFolderPath.replacingOccurrences(of:"file://", with:"")letfileManager =FileManager.default;//深度遍历letenumeratorAtPath = fileManager.enumerator(atPath: fileFolderStringPath)//过滤文件后缀letfilterPath =NSArray(array: (enumeratorAtPath?.allObjects)!).pathsMatchingExtensions(["h","m"])

    然后将注释排除在分析之外,这样做能够有效避免无用的解析。

    分析是否需要按照行来切割,在 @interface , @end 和 @ implementation , @end里面不需要换行,按照;符号,外部需要按行来。所以两种切割都需要。

    先定义语法标识符

    classSb:NSObject{publicstaticletadd ="+"publicstaticletminus ="-"publicstaticletrBktL ="("publicstaticletrBktR =")"publicstaticletasterisk ="*"publicstaticletcolon =":"publicstaticletsemicolon =";"publicstaticletdivide ="/"publicstaticletagBktL ="<"publicstaticletagBktR =">"publicstaticletquotM ="\""publicstaticletpSign ="#"publicstaticletbraceL ="{"publicstaticletbraceR ="}"publicstaticletbktL ="["publicstaticletbktR ="]"publicstaticletqM ="?"publicstaticletupArrow ="^"publicstaticletinteface ="@interface"publicstaticletimplementation ="@implementation"publicstaticletend ="@end"publicstaticletselector ="@selector"publicstaticletspace =" "publicstaticletnewLine ="\n"}

    接下来就要开始根据标记符号来进行切割分组了,使用 Scanner ,具体方式如下

    //根据代码文件解析出一个根据标记符切分的数组classfunccreateOCTokens(conent:String) -> [String]{varstr = conent    str =self.dislodgeAnnotaion(content: str)//开始扫描切割letscanner =Scanner(string: str)vartokens = [String]()//Todo:待处理符号,.letoperaters = [Sb.add,Sb.minus,Sb.rBktL,Sb.rBktR,Sb.asterisk,Sb.colon,Sb.semicolon,Sb.divide,Sb.agBktL,Sb.agBktR,Sb.quotM,Sb.pSign,Sb.braceL,Sb.braceR,Sb.bktL,Sb.bktR,Sb.qM]varoperatersString =""foropinoperaters {        operatersString = operatersString.appending(op)    }varset=CharacterSet()set.insert(charactersIn: operatersString)set.formUnion(CharacterSet.whitespacesAndNewlines)while!scanner.isAtEnd {foroperaterinoperaters {if(scanner.scanString(operater, into:nil)) {                tokens.append(operater)            }        }varresult:NSString?        result =nil;ifscanner.scanUpToCharacters(from:set, into: &result) {            tokens.append(resultas!String)        }    }    tokens = tokens.filter{        $0!=Sb.space    }returntokens;}

    行解析的方法

    //根据代码文件解析出一个根据行切分的数组classfunccreateOCLines(content:String) -> [String]{varstr = content    str =self.dislodgeAnnotaion(content: str)letstrArr = str.components(separatedBy:CharacterSet.newlines)returnstrArr}

    根据结构将定义的方法取出 🤖

    - (id)initWithMemoryCapacity:(NSUInteger)memoryCapacity diskCapacity:(NSUInteger)diskCapacity diskPath:(NSString*)path cacheTime:(NSInteger)cacheTime subDirectory:(NSString*)subDirectory;

    这里按照语法规则顺序取出即可,将方法名,返回类型,参数名,参数类型记录。这里需要注意 Block 类型的参数

    - (STMPartMaker *(^)(STMPartColorType))colorTypeIs;

    这种类型中还带有括号的语法的解析,这里用到的方法是对括号进行计数,左括号加一右括号减一的方式取得完整方法。

    获得这些数据后就可以开始检索定义的方法了。我写了一个类专门用来获得所有定义的方法

    classfuncparsingWithArray(arr:Array) ->Method{varmtd =Method()varreturnTypeTf =false//是否取得返回类型varparsingTf =false//解析中varbracketCount =0//括弧计数varstep =0//1获取参数名,2获取参数类型,3获取iNamevartypes = [String]()varmethodParam =MethodParam()//print("\(arr)")forvartkinarr {        tk = tk.replacingOccurrences(of:Sb.newLine, with:"")if(tk ==Sb.semicolon || tk ==Sb.braceL) && step !=1{varshouldAdd =falseifmtd.params.count>1{//处理这种- (void)initWithC:(type)m m2:(type2)i, ... NS_REQUIRES_NIL_TERMINATION;入参为多参数情况ifmethodParam.type.characters.count>0{                    shouldAdd =true}            }else{                shouldAdd =true}ifshouldAdd {                mtd.params.append(methodParam)                mtd.pnameId = mtd.pnameId.appending("\(methodParam.name):")            }                    }elseiftk ==Sb.rBktL {            bracketCount +=1parsingTf =true}elseiftk ==Sb.rBktR {            bracketCount -=1ifbracketCount ==0{vartypeString =""fortypeTkintypes {                    typeString = typeString.appending(typeTk)                }if!returnTypeTf {//完成获取返回mtd.returnType = typeString                    step =1returnTypeTf =true}else{ifstep ==2{                        methodParam.type = typeString                        step =3}                                    }//括弧结束后的重置工作parsingTf =falsetypes = []            }        }elseifparsingTf {            types.append(tk)//todo:返回block类型会使用.设置值的方式,目前获取用过方法方式没有.这种的解析,暂时作为iftk ==Sb.upArrow {                mtd.returnTypeBlockTf =true}        }elseiftk ==Sb.colon {            step =2}elseifstep ==1{iftk =="initWithCoordinate"{//}            methodParam.name = tk            step =0}elseifstep ==3{            methodParam.iName = tk            step =1mtd.params.append(methodParam)            mtd.pnameId = mtd.pnameId.appending("\(methodParam.name):")            methodParam =MethodParam()        }elseiftk !=Sb.minus && tk !=Sb.add {            methodParam.name = tk        }            }//遍历returnmtd}

    这个方法大概的思路就是根据标记符设置不同的状态,然后将获取的信息放入定义的结构中。

    使用过的方法的解析 😱

    进行使用过的方法解析前需要处理的事情

    @“…” 里面的数据,因为这里面是允许我们定义的标识符出现的。

    递归出文件中 import 所有的类,根据对类的使用可以清除无用的 import

    继承链的获取。

    解析获取实例化了的成员变量列表。在解析时需要依赖列表里的成员变量名和变量的类进行方法的完整获取。

    简单的方法

    [view update:status animation:YES];

    从左到右按照 : 符号获取

    方法嵌套调用,下面这种情况如何解析出

    @weakify(self);[[[[[[SMNetManager shareInstance] fetchAllFeedWithModelArray:self.feeds] map:^id(NSNumber*value) {    @strongify(self);NSUIntegerindex = [value integerValue];self.feeds[index] = [SMNetManager shareInstance].feeds[index];returnself.feeds[index];}] doCompleted:^{//抓完所有的feeds@strongify(self);NSLog(@"fetch complete");//完成置为默认状态self.tbHeaderLabel.text =@"";self.tableView.tableHeaderView = [[UIViewalloc] init];self.fetchingCount =0;    [UIApplicationsharedApplication].networkActivityIndicatorVisible =NO;//下拉刷新关闭[self.tableView.mj_header endRefreshing];//更新列表[self.tableView reloadData];//检查是否需要增加源if([SMFeedStore defaultFeeds].count >self.feeds.count) {self.feeds = [SMFeedStore defaultFeeds];        [selffetchAllFeeds];    }}] deliverOn:[RACScheduler mainThreadScheduler]] subscribeNext:^(SMFeedModel *feedModel) {//抓完一个@strongify(self);self.tableView.tableHeaderView =self.tbHeaderView;//显示抓取状态self.fetchingCount +=1;self.tbHeaderLabel.text = [NSStringstringWithFormat:@"正在获取%@...(%lu/%lu)",feedModel.title,(unsignedlong)self.fetchingCount,(unsignedlong)self.feeds.count];    [self.tableView reloadData];}];

    一开始会想到使用递归,以前我做 STMAssembleView 时就是使用的递归,这样时间复杂度就会是 O(nlogn) ,这次我换了个思路,将复杂度降低到了 n ,思路大概是 创建一个字典,键值就是深度,从左到右深度的增加根据 [ 符号,减少根据 ] 符号,值会在 [时创建一个 Method 结构体,根据]来完成结构体,将其添加到 methods 数组中 。

    具体实现如下

    classfuncparsing(contentArr:Array,inMethod:Method) ->Method{varmtdIn = inMethod//处理用过的方法//todo:还要过滤@""这种情况varpsBrcStep =0varuMtdDic = [Int:Method]()varpreTk =""//处理?:这种条件判断简写方式varpsCdtTf =falsevarpsCdtStep =0//判断selectorvarpsSelectorTf =falsevarpreSelectorTk =""varselectorMtd =Method()varselectorMtdPar =MethodParam()        uMtdDic[psBrcStep] =Method()//初始时就实例化一个method,避免在define里定义只定义]符号forvartkincontentArr {//selector处理ifpsSelectorTf {iftk ==Sb.colon {                selectorMtdPar.name = preSelectorTk                selectorMtd.params.append(selectorMtdPar)                selectorMtd.pnameId +="\(selectorMtdPar.name):"}elseiftk ==Sb.rBktR {                mtdIn.usedMethod.append(selectorMtd)                psSelectorTf =falseselectorMtd =Method()                selectorMtdPar =MethodParam()            }else{                preSelectorTk = tk            }continue}iftk ==Sb.selector {            psSelectorTf =trueselectorMtd =Method()            selectorMtdPar =MethodParam()continue}//通常处理iftk ==Sb.bktL {ifpsCdtTf {                psCdtStep +=1}            psBrcStep +=1uMtdDic[psBrcStep] =Method()        }elseiftk ==Sb.bktR {ifpsCdtTf {                psCdtStep -=1}if(uMtdDic[psBrcStep]?.params.count)! >0{                mtdIn.usedMethod.append(uMtdDic[psBrcStep]!)            }            psBrcStep -=1//[]不配对的容错处理ifpsBrcStep <0{                psBrcStep =0}                    }elseiftk ==Sb.colon {//条件简写情况处理ifpsCdtTf && psCdtStep ==0{                psCdtTf =falsecontinue}//dictionary情况处理@"key":@"value"ifpreTk ==Sb.quotM || preTk =="respondsToSelector"{continue}letprm =MethodParam()            prm.name = preTkifprm.name !=""{                uMtdDic[psBrcStep]?.params.append(prm)                uMtdDic[psBrcStep]?.pnameId = (uMtdDic[psBrcStep]?.pnameId.appending("\(prm.name):"))!            }        }elseiftk ==Sb.qM {            psCdtTf =true}else{            tk = tk.replacingOccurrences(of:Sb.newLine, with:"")            preTk = tk        }    }returnmtdIn}

    在设置 Method 结构体时将参数名拼接起来成为 Method 的识别符用于后面处理时的快速比对。

    解析使用过的方法时有几个问题需要注意下

    1.在方法内使用的方法,会有 respondsToSelector , @selector 还有条件简写语法的情况需要单独处理下。

    2.在 #define 里定义使用了方法

    #defineCLASS_VALUE(x)    [NSValue valueWithNonretainedObject:(x)]

    找出无用方法 😄

    获取到所有使用方法后进行去重,和定义方法进行匹对求出差集,即全部未使用的方法。

    去除无用方法 😎

    比对后获得无用方法后就要开始注释掉他们了。遍历未使用的方法,根据先前 Method结构体中定义了方法所在文件路径,根据文件集结构和File的结构体,可以避免 IO ,直接获取方法对应的文件内容和路径。

    对文件内容进行行切割,逐行检测方法名和参数,匹对时开始对行加上注释, h 文件已;符号为结束, m 文件会对大括号进行计数,逐行注释。实现的方法具体如下:

    //删除指定的一组方法classfuncdelete(methods:[Method]){print("无用方法")foraMethodinmethods {print("\(File.desDefineMethodParams(paramArr: aMethod.params))")//开始删除//continuevarhContent =""varmContent =""varmFilePath = aMethod.filePathifaMethod.filePath.hasSuffix(".h") {            hContent =try!String(contentsOf:URL(string:aMethod.filePath)!, encoding:String.Encoding.utf8)//todo:因为先处理了h文件的情况mFilePath = aMethod.filePath.trimmingCharacters(in:CharacterSet(charactersIn:"h"))//去除头尾字符集mFilePath = mFilePath.appending("m")        }ifmFilePath.hasSuffix(".m") {do{                mContent =tryString(contentsOf:URL(string:mFilePath)!, encoding:String.Encoding.utf8)            }catch{                mContent =""}        }lethContentArr = hContent.components(separatedBy:CharacterSet.newlines)letmContentArr = mContent.components(separatedBy:CharacterSet.newlines)//print(mContentArr)//----------------h文件------------------varpsHMtdTf =falsevarhMtds = [String]()varhMtdStr =""varhMtdAnnoStr =""varhContentCleaned =""forhOneLineinhContentArr {varline = hOneLine.trimmingCharacters(in:CharacterSet.whitespacesAndNewlines)ifline.hasPrefix(Sb.minus) || line.hasPrefix(Sb.add) {                psHMtdTf =truehMtds +=self.createOCTokens(conent: line)                hMtdStr = hMtdStr.appending(hOneLine +Sb.newLine)                hMtdAnnoStr +="//-----由SMCheckProject工具删除-----\n//"hMtdAnnoStr += hOneLine +Sb.newLine                line =self.dislodgeAnnotaionInOneLine(content: line)                line = line.trimmingCharacters(in:CharacterSet.whitespacesAndNewlines)            }elseifpsHMtdTf {                hMtds +=self.createOCTokens(conent: line)                hMtdStr = hMtdStr.appending(hOneLine +Sb.newLine)                hMtdAnnoStr +="//"+ hOneLine +Sb.newLine                line =self.dislodgeAnnotaionInOneLine(content: line)                line = line.trimmingCharacters(in:CharacterSet.whitespacesAndNewlines)            }else{                hContentCleaned += hOneLine +Sb.newLine            }ifline.hasSuffix(Sb.semicolon) && psHMtdTf{                psHMtdTf =falseletmethodPnameId =ParsingMethod.parsingWithArray(arr: hMtds).pnameIdifaMethod.pnameId == methodPnameId {                    hContentCleaned += hMtdAnnoStr                }else{                    hContentCleaned += hMtdStr                }                hMtdAnnoStr =""hMtdStr =""hMtds = []            }        }//删除无用函数try! hContentCleaned.write(to:URL(string:aMethod.filePath)!, atomically:false, encoding:String.Encoding.utf8)//----------------m文件----------------varmDeletingTf =falsevarmBraceCount =0varmContentCleaned =""varmMtdStr =""varmMtdAnnoStr =""varmMtds = [String]()varpsMMtdTf =falseformOneLineinmContentArr {letline = mOneLine.trimmingCharacters(in:CharacterSet.whitespacesAndNewlines)ifmDeletingTf {letlTokens =self.createOCTokens(conent: line)                mMtdAnnoStr +="//"+ mOneLine +Sb.newLinefortkinlTokens {iftk ==Sb.braceL {                        mBraceCount +=1}iftk ==Sb.braceR {                        mBraceCount -=1ifmBraceCount ==0{                            mContentCleaned = mContentCleaned.appending(mMtdAnnoStr)                            mMtdAnnoStr =""mDeletingTf =false}                    }                }continue}ifline.hasPrefix(Sb.minus) || line.hasPrefix(Sb.add) {                psMMtdTf =truemMtds +=self.createOCTokens(conent: line)                mMtdStr = mMtdStr.appending(mOneLine +Sb.newLine)                mMtdAnnoStr +="//-----由SMCheckProject工具删除-----\n//"+ mOneLine +Sb.newLine            }elseifpsMMtdTf {                mMtdStr = mMtdStr.appending(mOneLine +Sb.newLine)                mMtdAnnoStr +="//"+ mOneLine +Sb.newLine                mMtds +=self.createOCTokens(conent: line)            }else{                mContentCleaned = mContentCleaned.appending(mOneLine +Sb.newLine)            }ifline.hasSuffix(Sb.braceL) && psMMtdTf {                psMMtdTf =falseletmethodPnameId =ParsingMethod.parsingWithArray(arr: mMtds).pnameIdifaMethod.pnameId == methodPnameId {                    mDeletingTf =truemBraceCount +=1mContentCleaned = mContentCleaned.appending(mMtdAnnoStr)                }else{                    mContentCleaned = mContentCleaned.appending(mMtdStr)                }                mMtdStr =""mMtdAnnoStr =""mMtds = []            }        }//m文件//删除无用函数ifmContent.characters.count>0{try! mContentCleaned.write(to:URL(string:mFilePath)!, atomically:false, encoding:String.Encoding.utf8)        }    }}

    完整代码在:https://github.com/ming1016/SMCheckProject 这里。

    后记 🦁

    有了这样的结构数据就可以模拟更多人工检测的方式来检测项目。

    通过获取的方法结合获取类里面定义的局部变量和全局变量,在解析过程中模拟引用的计数来分析循环引用等等类似这样的检测。

    通过获取的类的完整结构还能够将其转成JavaScriptCore能解析的js语法文件等等。

    对于APP瘦身的一些想法 👽

    瘦身应该从平时开发时就需要注意。除了功能和组件上的复用外还需要对堆栈逻辑进行封装以达到代码压缩的效果。

    比如使用ReactiveCocoa和RxSwift这样的函数响应式编程库提供的方法和编程模式进行

    对于UI的视图逻辑可以使用一套统一逻辑压缩代码使用DSL来简化写法等。

    小礼物走一走,来简书关注我

    赞赏支持

     开发

    © 著作权归作者所有

    举报文章

    关注星光社的戴铭 

    写了 126812 字,被 3032 人关注,获得了 1731 个喜欢

    微博:@戴铭,github帐号ming1016,喜欢画画,instagram帐号ming1016,qq:36270359

    喜欢

    29

    更多分享

    登录 后发表评论

    8条评论 只看作者

    按时间倒序按时间正序

    写太多bug了

    4楼 · 2016.12.30 16:11

    bogon:SMCheckProject chengqian$ pod install --no-repo-update

    Analyzing dependencies

    [!] Unable to satisfy the following requirements:

    - `SnapKit (~> 3.0.2)` required by `Podfile`

    None of your spec sources contain a spec satisfying the dependency: `SnapKit (~> 3.0.2)`.

    You have either:

    * out-of-date source repos which you can update with `pod repo update`.

    * mistyped the name or version.

    * not added the source repo that hosts the Podspec to your Podfile.

    Note: as of CocoaPods 1.0, `pod repo update` does not happen on `pod install` by default.

      回复

    星光社的戴铭

     @CharType pod install 一下

    2016.12.31 16:14  回复

    写太多bug了

     @星光社的戴铭 谢谢

    2017.01.03 21:17  回复

    厚脸皮

     拖入工程目录之后,查找不出来

    2017.01.22 10:28  回复

     添加新评论

    枫_LY

    3楼 · 2016.11.23 15:05

    工程报错, 不能运行diff: /../Podfile.lock: No such file or directory

    diff: /Manifest.lock: No such file or directory

    error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.

      回复

    星光社的戴铭

     @枫_LY 用cocoapods更新下

    2016.11.24 10:47  回复

     添加新评论

    I往往I

    2楼 · 2016.10.31 14:37

    本人swift知识尚浅,老是容易在这崩溃ParsingMethodContent中

    34行

    if (uMtdDic[psBrcStep]?.params.count)! > 0 {

      回复

    星光社的戴铭

     @I往往I 问题已修复,可以拉下最新代码

    2016.11.10 19:50  回复

     添加新评论

    被以下专题收入,发现更多相似内容

    移动前沿

    iOS 实用技术

    征服iOS

    App性能监控

    项目笔记

    电脑相关

    待阅读的文章

    展开更多 

    Spring Cloud

    Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智能路由,微代理,控制总线)。分布式系统的协调导致了样板模式, 使用Spring Cloud开发人员可以快速地支持实现这些模式的服务和应用程序。他们将在任何分布式...

     卡卡罗2017

    百战程序员V1.2——尚学堂旗下高端培训_ Java1573题

    百战程序员_ Java1573题 QQ群:561832648489034603 掌握80%年薪20万掌握50%年薪10万 全程项目穿插, 从易到难,含17个项目视频和资料持续更新,请关注www.itbaizhan.com 国内最牛七星级团队马士兵、高淇等11位十年开发经验专...

     Albert陈凯

    掘金 Android 文章精选合集

    用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金Cover 有什么料? 从这篇文章中你能获得这些料: 知道setContentView()之后发生了什么? ... Android 获取 View 宽高的常用正确方式,避免为零 - 掘金相信有很多朋友...

     掘金官方

    Android - 收藏集 

    用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你能获得这些料: 知道setContentView()之后发生了什么? ... Android 获取 View 宽高的常用正确方式,避免为零 - 掘金 相信有很多...

     passiontim

    无标题文章

    转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一. Runtime简介Runtime 又叫运行时,是一套底层的 C 语言 API,是 iOS 系统的核心之一。开发者在编码过程中,可以给任意一个对象发送消息,在编...

     色色的小法师

    理想主义还是精神病?——纳粹副元首1941年只身飞赴英国之谜

    冯涛 1941年5月10日,纳粹德国副元首鲁道夫·赫斯(Rudolf Hess)飞赴英国并被俘的消息,如同一枚重磅炸弹在德国和英国政府间炸开。这一看似疯狂的事件的来龙去脉究竟如何?赫斯难道真的如果纳粹德国所宣称的那样疯了吗? “2016年5月20日,大德意志帝国与大英帝国与...

     崎峻军史周刊

    短篇 《余路》

    1 起床 ,床上粉丝的被子缩成一团,红色地毯上都是狗毛和薯片碎屑,在看床上空无一人,那几个酒瓶乱七八糟的在地下。 滋吖一声,木门就被打开了,随处传来韩冰的狮子哄: 韩鑫!你丫的,脑子有病吧,不在床上睡又跑到地下去了。没给你说过么地下咱家宝在换肤呢。地上都是他的旧的皮肤,你还...

     Te小鱼儿er

    88/100-谢谢林先生,点亮颗颗心

    社会各界爱心人士: 6.22火灾,我的三个孩子和我的妻子永远离开我了。我的原本幸福美满的家庭毁于一旦,至今我都不愿相信这一切真的发生了。接下去的人生该怎么过?最近这些天,这个念头总是在我脑子里回旋。我坚信,真相会被揭示!我也坚信,善恶终有报!为了寻求真相,为了给逝去的亲人讨...

     桂霏是人才

    一句话告诉你十二星座遇到真爱时的表现

    之前小桃看到一句话:如果我有尾巴的话,看见喜欢的人一定会止不住的摇。 每个人遇到真爱时的反应都不一样,十二星座不同的人,他们遇到真爱时会怎样表现呢?一起来看看吧~ 白羊座 会认真看着你的眼睛说:“我爱你” 金牛座 愿意带你回家 双子座 主动上交工资卡 巨蟹座 缠着你让你带他...

     照亮天空星座

    相关文章

      网友评论

          本文标题:App瘦身

          本文链接:https://www.haomeiwen.com/subject/chabwftx.html