Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >使用Swift3开发了个macOS的程序可以检测出objc项目中无用方法,然后一键全部清理

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

作者头像
用户7451029
发布于 2020-06-16 08:32:25
发布于 2020-06-16 08:32:25
62700
代码可运行
举报
文章被收录于专栏:戴铭的博客戴铭的博客
运行总次数:0
代码可运行

当项目越来越大,引入第三方库越来越多,上架的APP体积也会越来越大,对于用户来说体验必定是不好的。在清理资源,编译选项优化,清理无用类等完成后,能够做而且效果会比较明显的就只有清理无用函数了。现有一种方案是根据Linkmap文件取到objc的所有类方法和实例方法。再用工具逆向可执行文件里引用到的方法名,求个差集列出无用方法。这个方案有些比较麻烦的地方,因为检索出的无用方法没法确定能够直接删除,还需要挨个检索人工判断是否可以删除,这样每次要清理时都需要这样人工排查一遍是非常耗时耗力的。

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

设置结构体 ?

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct objc_object {  
    Class isa  OBJC_ISA_AVAILABILITY;
};

/*类*/
struct objc_class {  
    Class isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
    Class super_class;
    const char *name;
    long version;
    long info;
    long instance_size;
    struct objc_ivar_list *ivars;
    struct objc_method_list **methodLists;
    struct objc_cache *cache;
    struct objc_protocol_list *protocols;
#endif
};

/*成员变量列表*/
struct objc_ivar_list {
    int ivar_count               
#ifdef __LP64__
    int space                    
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1]
}      

/*成员变量结构体*/
struct objc_ivar {
    char *ivar_name
    char *ivar_type
    int ivar_offset
#ifdef __LP64__
    int space      
#endif
}    

/*方法列表*/
struct objc_method_list {  
    struct objc_method_list *obsolete;
    int method_count;

#ifdef __LP64__
    int space;
#endif
    /* variable length structure */
    struct objc_method method_list[1];
};

/*方法结构体*/
struct objc_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 创建类似的结构体,做些修改

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//文件
class File: NSObject {
    //文件
    public var type = FileType.FileH
    public var name = ""
    public var content = ""
    public var methods = [Method]() //所有方法
    public var imports = [Import]() //引入类
}

//引入
struct Import {
    public var fileName = ""
}

//对象
class Object {
    public var name = ""
    public var superObject = ""
    public var properties = [Property]()
    public var methods = [Method]()
}

//成员变量
struct Property {
    public var name = ""
    public var type = ""
}

struct Method {
    public var classMethodTf = false //+ or -
    public var returnType = ""
    public var returnTypePointTf = false
    public var returnTypeBlockTf = false
    public var params = [MethodParam]()
    public var usedMethod = [Method]()
    public var filePath = "" //定义方法的文件路径,方便修改文件使用
    public var pnameId = ""  //唯一标识,便于快速比较
}

class MethodParam: NSObject {
    public var name = ""
    public var type = ""
    public var typePointTf = false
    public var iName = ""
}

class Type: NSObject {
    //todo:更多类型
    public var name = ""
    public var type = 0 //0是值类型 1是指针
}
```swift

## 开始语法解析 ?
首先遍历目录下所有的文件。
```swift
let fileFolderPath = self.selectFolder()
let fileFolderStringPath = fileFolderPath.replacingOccurrences(of: "file://", with: "")
let fileManager = FileManager.default;
//深度遍历
let enumeratorAtPath = fileManager.enumerator(atPath: fileFolderStringPath)
//过滤文件后缀
let filterPath = NSArray(array: (enumeratorAtPath?.allObjects)!).pathsMatchingExtensions(["h","m"])

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

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

先定义语法标识符

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Sb: NSObject {
    public static let add = "+"
    public static let minus = "-"
    public static let rBktL = "("
    public static let rBktR = ")"
    public static let asterisk = "*"
    public static let colon = ":"
    public static let semicolon = ";"
    public static let divide = "/"
    public static let agBktL = "<"
    public static let agBktR = ">"
    public static let quotM = "\""
    public static let pSign = "#"
    public static let braceL = "{"
    public static let braceR = "}"
    public static let bktL = "["
    public static let bktR = "]"
    public static let qM = "?"
    public static let upArrow = "^"

    public static let inteface = "@interface"
    public static let implementation = "@implementation"
    public static let end = "@end"
    public static let selector = "@selector"

    public static let space = " "
    public static let newLine = "\n"
}

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//根据代码文件解析出一个根据标记符切分的数组
class func createOCTokens(conent:String) -> [String] {
    var str = conent

    str = self.dislodgeAnnotaion(content: str)

    //开始扫描切割
    let scanner = Scanner(string: str)
    var tokens = [String]()
    //Todo:待处理符号,.
    let operaters = [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]
    var operatersString = ""
    for op in operaters {
        operatersString = operatersString.appending(op)
    }

    var set = CharacterSet()
    set.insert(charactersIn: operatersString)
    set.formUnion(CharacterSet.whitespacesAndNewlines)

    while !scanner.isAtEnd {
        for operater in operaters {
            if (scanner.scanString(operater, into: nil)) {
                tokens.append(operater)
            }
        }

        var result:NSString?
        result = nil;
        if scanner.scanUpToCharacters(from: set, into: &result) {
            tokens.append(result as! String)
        }
    }
    tokens = tokens.filter {
        $0 != Sb.space
    }
    return tokens;
}

行解析的方法

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//根据代码文件解析出一个根据行切分的数组
class func createOCLines(content:String) -> [String] {
    var str = content
    str = self.dislodgeAnnotaion(content: str)
    let strArr = str.components(separatedBy: CharacterSet.newlines)
    return strArr
}

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
- (id)initWithMemoryCapacity:(NSUInteger)memoryCapacity diskCapacity:(NSUInteger)diskCapacity diskPath:(NSString *)path cacheTime:(NSInteger)cacheTime subDirectory:(NSString*)subDirectory;

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
- (STMPartMaker *(^)(STMPartColorType))colorTypeIs;

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class func parsingWithArray(arr:Array<String>) -> Method {
    var mtd = Method()
    var returnTypeTf = false //是否取得返回类型
    var parsingTf = false //解析中
    var bracketCount = 0 //括弧计数
    var step = 0 //1获取参数名,2获取参数类型,3获取iName
    var types = [String]()
    var methodParam = MethodParam()
    //print("\(arr)")
    for var tk in arr {
        tk = tk.replacingOccurrences(of: Sb.newLine, with: "")
        if (tk == Sb.semicolon || tk == Sb.braceL) && step != 1 {
            var shouldAdd = false

            if mtd.params.count > 1 {
                //处理这种- (void)initWithC:(type)m m2:(type2)i, ... NS_REQUIRES_NIL_TERMINATION;入参为多参数情况
                if methodParam.type.characters.count > 0 {
                    shouldAdd = true
                }
            } else {
                shouldAdd = true
            }
            if shouldAdd {
                mtd.params.append(methodParam)
                mtd.pnameId = mtd.pnameId.appending("\(methodParam.name):")
            }

        } else if tk == Sb.rBktL {
            bracketCount += 1
            parsingTf = true
        } else if tk == Sb.rBktR {
            bracketCount -= 1
            if bracketCount == 0 {
                var typeString = ""
                for typeTk in types {
                    typeString = typeString.appending(typeTk)
                }
                if !returnTypeTf {
                    //完成获取返回
                    mtd.returnType = typeString
                    step = 1
                    returnTypeTf = true
                } else {
                    if step == 2 {
                        methodParam.type = typeString
                        step = 3
                    }

                }
                //括弧结束后的重置工作
                parsingTf = false
                types = []
            }
        } else if parsingTf {
            types.append(tk)
            //todo:返回block类型会使用.设置值的方式,目前获取用过方法方式没有.这种的解析,暂时作为
            if tk == Sb.upArrow {
                mtd.returnTypeBlockTf = true
            }
        } else if tk == Sb.colon {
            step = 2
        } else if step == 1 {
            if tk == "initWithCoordinate" {
                //
            }
            methodParam.name = tk
            step = 0
        } else if step == 3 {
            methodParam.iName = tk
            step = 1
            mtd.params.append(methodParam)
            mtd.pnameId = mtd.pnameId.appending("\(methodParam.name):")
            methodParam = MethodParam()
        } else if tk != Sb.minus && tk != Sb.add {
            methodParam.name = tk
        }

    }//遍历

    return mtd
}

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

使用过的方法的解析 ?

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

  • @“…” 里面的数据,因为这里面是允许我们定义的标识符出现的。
  • 递归出文件中 import 所有的类,根据对类的使用可以清除无用的 import
  • 继承链的获取。
  • 解析获取实例化了的成员变量列表。在解析时需要依赖列表里的成员变量名和变量的类进行方法的完整获取。

简单的方法

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[view update:status animation:YES];

从左到右按照 : 符号获取

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@weakify(self);
[[[[[[SMNetManager shareInstance] fetchAllFeedWithModelArray:self.feeds] map:^id(NSNumber *value) {
    @strongify(self);
    NSUInteger index = [value integerValue];
    self.feeds[index] = [SMNetManager shareInstance].feeds[index];
    return self.feeds[index];
}] doCompleted:^{
    //抓完所有的feeds
    @strongify(self);
    NSLog(@"fetch complete");
    //完成置为默认状态
    self.tbHeaderLabel.text = @"";
    self.tableView.tableHeaderView = [[UIView alloc] init];
    self.fetchingCount = 0;
    [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
    //下拉刷新关闭
    [self.tableView.mj_header endRefreshing];
    //更新列表
    [self.tableView reloadData];
    //检查是否需要增加源
    if ([SMFeedStore defaultFeeds].count > self.feeds.count) {
        self.feeds = [SMFeedStore defaultFeeds];
        [self fetchAllFeeds];
    }
}] deliverOn:[RACScheduler mainThreadScheduler]] subscribeNext:^(SMFeedModel *feedModel) {
    //抓完一个
    @strongify(self);
    self.tableView.tableHeaderView = self.tbHeaderView;
    //显示抓取状态
    self.fetchingCount += 1;
    self.tbHeaderLabel.text = [NSString stringWithFormat:@"正在获取%@...(%lu/%lu)",feedModel.title,(unsigned long)self.fetchingCount,(unsigned long)self.feeds.count];
    [self.tableView reloadData];
}];

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

具体实现如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class func parsing(contentArr:Array<String>, inMethod:Method) -> Method {
    var mtdIn = inMethod
    //处理用过的方法
    //todo:还要过滤@""这种情况
    var psBrcStep = 0
    var uMtdDic = [Int:Method]()
    var preTk = ""
    //处理?:这种条件判断简写方式
    var psCdtTf = false
    var psCdtStep = 0
    //判断selector
    var psSelectorTf = false
    var preSelectorTk = ""
    var selectorMtd = Method()
    var selectorMtdPar = MethodParam()

    uMtdDic[psBrcStep] = Method() //初始时就实例化一个method,避免在define里定义只定义]符号

    for var tk in contentArr {
        //selector处理
        if psSelectorTf {
            if tk == Sb.colon {
                selectorMtdPar.name = preSelectorTk
                selectorMtd.params.append(selectorMtdPar)
                selectorMtd.pnameId += "\(selectorMtdPar.name):"
            } else if tk == Sb.rBktR {
                mtdIn.usedMethod.append(selectorMtd)
                psSelectorTf = false
                selectorMtd = Method()
                selectorMtdPar = MethodParam()
            } else {
                preSelectorTk = tk
            }
            continue
        }
        if tk == Sb.selector {
            psSelectorTf = true
            selectorMtd = Method()
            selectorMtdPar = MethodParam()
            continue
        }
        //通常处理
        if tk == Sb.bktL {
            if psCdtTf {
                psCdtStep += 1
            }
            psBrcStep += 1
            uMtdDic[psBrcStep] = Method()
        } else if tk == Sb.bktR {
            if psCdtTf {
                psCdtStep -= 1
            }
            if (uMtdDic[psBrcStep]?.params.count)! > 0 {
                mtdIn.usedMethod.append(uMtdDic[psBrcStep]!)
            }
            psBrcStep -= 1
            //[]不配对的容错处理
            if psBrcStep < 0 {
                psBrcStep = 0
            }

        } else if tk == Sb.colon {
            //条件简写情况处理
            if psCdtTf && psCdtStep == 0 {
                psCdtTf = false
                continue
            }
            //dictionary情况处理@"key":@"value"
            if preTk == Sb.quotM || preTk == "respondsToSelector" {
                continue
            }
            let prm = MethodParam()
            prm.name = preTk
            if prm.name != "" {
                uMtdDic[psBrcStep]?.params.append(prm)
                uMtdDic[psBrcStep]?.pnameId = (uMtdDic[psBrcStep]?.pnameId.appending("\(prm.name):"))!
            }
        } else if tk == Sb.qM {
            psCdtTf = true
        } else {
            tk = tk.replacingOccurrences(of: Sb.newLine, with: "")
            preTk = tk
        }
    }

    return mtdIn
}

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

解析使用过的方法时有几个问题需要注意下 1.在方法内使用的方法,会有 respondsToSelector@selector 还有条件简写语法的情况需要单独处理下。 2.在 #define 里定义使用了方法

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#define CLASS_VALUE(x)    [NSValue valueWithNonretainedObject:(x)]

找出无用方法 ?

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

去除无用方法 ?

比对后获得无用方法后就要开始注释掉他们了。遍历未使用的方法,根据先前 Method 结构体中定义了方法所在文件路径,根据文件集结构和File的结构体,可以避免 IO ,直接获取方法对应的文件内容和路径。 对文件内容进行行切割,逐行检测方法名和参数,匹对时开始对行加上注释, h 文件已;符号为结束, m 文件会对大括号进行计数,逐行注释。实现的方法具体如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//删除指定的一组方法
class func delete(methods:[Method]) {
    print("无用方法")
    for aMethod in methods {
        print("\(File.desDefineMethodParams(paramArr: aMethod.params))")

        //开始删除
        //continue
        var hContent = ""
        var mContent = ""
        var mFilePath = aMethod.filePath
        if aMethod.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")
        }
        if mFilePath.hasSuffix(".m") {
            do {
                mContent = try String(contentsOf: URL(string:mFilePath)!, encoding: String.Encoding.utf8)
            } catch {
                mContent = ""
            }

        }

        let hContentArr = hContent.components(separatedBy: CharacterSet.newlines)
        let mContentArr = mContent.components(separatedBy: CharacterSet.newlines)
        //print(mContentArr)
        //----------------h文件------------------
        var psHMtdTf = false
        var hMtds = [String]()
        var hMtdStr = ""
        var hMtdAnnoStr = ""
        var hContentCleaned = ""
        for hOneLine in hContentArr {
            var line = hOneLine.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)

            if line.hasPrefix(Sb.minus) || line.hasPrefix(Sb.add) {
                psHMtdTf = true
                hMtds += 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)
            } else if psHMtdTf {
                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
            }

            if line.hasSuffix(Sb.semicolon) && psHMtdTf{
                psHMtdTf = false

                let methodPnameId = ParsingMethod.parsingWithArray(arr: hMtds).pnameId
                if aMethod.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文件----------------
        var mDeletingTf = false
        var mBraceCount = 0
        var mContentCleaned = ""
        var mMtdStr = ""
        var mMtdAnnoStr = ""
        var mMtds = [String]()
        var psMMtdTf = false
        for mOneLine in mContentArr {
            let line = mOneLine.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)

            if mDeletingTf {
                let lTokens = self.createOCTokens(conent: line)
                mMtdAnnoStr += "//" + mOneLine + Sb.newLine
                for tk in lTokens {
                    if tk == Sb.braceL {
                        mBraceCount += 1
                    }
                    if tk == Sb.braceR {
                        mBraceCount -= 1
                        if mBraceCount == 0 {
                            mContentCleaned = mContentCleaned.appending(mMtdAnnoStr)
                            mMtdAnnoStr = ""
                            mDeletingTf = false
                        }
                    }
                }

                continue
            }


            if line.hasPrefix(Sb.minus) || line.hasPrefix(Sb.add) {
                psMMtdTf = true
                mMtds += self.createOCTokens(conent: line)
                mMtdStr = mMtdStr.appending(mOneLine + Sb.newLine)
                mMtdAnnoStr += "//-----由SMCheckProject工具删除-----\n//" + mOneLine + Sb.newLine
            } else if psMMtdTf {
                mMtdStr = mMtdStr.appending(mOneLine + Sb.newLine)
                mMtdAnnoStr += "//" + mOneLine + Sb.newLine
                mMtds += self.createOCTokens(conent: line)
            } else {
                mContentCleaned = mContentCleaned.appending(mOneLine + Sb.newLine)
            }

            if line.hasSuffix(Sb.braceL) && psMMtdTf {
                psMMtdTf = false
                let methodPnameId = ParsingMethod.parsingWithArray(arr: mMtds).pnameId
                if aMethod.pnameId == methodPnameId {
                    mDeletingTf = true
                    mBraceCount += 1
                    mContentCleaned = mContentCleaned.appending(mMtdAnnoStr)
                } else {
                    mContentCleaned = mContentCleaned.appending(mMtdStr)
                }
                mMtdStr = ""
                mMtdAnnoStr = ""
                mMtds = []
            }

        } //m文件

        //删除无用函数
        if mContent.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来简化写法等。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2016-11-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
只需4步!带你成功将小程序转为APP
自有APP不但有利于企业品牌推广,同时还更有利于收集用户相关数据,从而帮助企业及时调整发展方向和目标。而从0到1进行自有APP的开发其实是一项耗时耗力的工程,为了节约时间和成本,不少企业考虑直接将其现有微信小程序转换成商用APP,高效实现将客户从小程序引向APP。
Lydiasq
2022/12/14
4.1K2
只需4步!带你成功将小程序转为APP
不改代码就能将微信小程序生成商用App?
在知识爆炸的后信息社会,注意力资源已经成为十分稀缺的经济资源,经营注意力资源的产业如媒介、广告、体育、模特等获得迅猛发展,成为高利润的新兴产业群,注意力经济正在形成。注意力经济即以最大限度的吸引用户或消费者的注意力,通过培养潜在的消费群体,以期获得最大的未来商业利益的经济模式。
二山山记
2022/07/19
7990
原来微信小程序已经可以在自己的APP上架运行了
推荐一波移动开发领域热门的前端容器技术,绝对可以提高你的生产力,剩下来的时间来 mo鱼,岂不美哉 大家是不是潜意识一直觉得,只有那些超级APP才具备运行小程序的能力,而且日常生活中使用小程序场景最多的无非就是微信、支付宝、头条、百度这几个 APP,那你们有没有想过「自己的APP也可以具备小程序的运行能力」呢? 虽然互联网大厂并未将这部分小程序运行能力开放出来,但是我们也不必望而生羡,因为小程序技术不再是 BAT 的专属,市面上早就推出了类似技术能力,我们一般称之为小程序容器技术。 今天要给大家推荐的也正是
程序猿DD
2022/05/30
1.7K0
原来微信小程序已经可以在自己的APP上架运行了
小程序容器技术让混合App的开发效率大幅提升
Hybrid App(混合模式移动应用)是指介于Web-App、Native-App这两者之间的App,兼具“Native App良好用户交互体验的优势”和“Web App跨平台开发的优势”。
pak
2022/07/13
6250
小程序容器技术加入到混合App开发队伍
混合开发(Hybrid App开发):是指在开发一款App产品的时候,为了提高效率、节省成本而利用原生与H5的开发技术的混合应用。通俗点来说,这就是网页的模式,通常由“HTML5云网站+APP应用客户端”两部份构成。混合开发是一种取长补短的开发模式,原生代码部分利用Web View插件或者其它框架为H5提供容器,程序主要的业务实现、界面展示都是利用与H5相关的Web技术进行实现的。混合应用开发正是结合原生和HTML5开发的技术,取长补短的一种开发模式,原生代码部分利用WebView插件或者其它的框架为HTML5提供了一个容器,程序主要的业务实现、界面展示是利用H5相关的Web技术进行实现的。比如现在的京东、淘宝、今日头条等都是利用的混合开发模式
pak
2022/09/06
4450
「Native+小程序」的开发模式优势在哪?
Hybrid App(混合模式移动应用)是指介于Web-App、Native-App这两者之间的App,同时使用网页语言与程序语言开发,通过应用商店区分移动操作系统分发,用户需要安装使用的移动应用”。
二山山记
2022/11/09
7180
「Native+小程序」的开发模式优势在哪?
微信授权登录第三方APP小程序方法介绍
第三方登录,是指用户使用在其他程序上频繁使用的账号,来快速登录另一方程序,可以实现不用注册就快捷完成登录。
pak
2022/06/13
3.4K0
如何实现app与微信小程序的二维码聚合?
微信/支付宝收款码大家应该不会陌生,现在线下商户收款大多使用这个,只需扫同一个二维码,自动识别打开各端小程序。这种方式一般统称为二维码聚合或者说是“一码通”能力,一个收款码,支持多种客户端,主流是微信、支付宝,现在常见还会支持美团、饿了么、银联、QQ 等。用户选择任一支持的客户端扫码,都能完成支付,再也不用纠结扫错码的尴尬。有没有很神奇?其实底层原理很简单,看完你就明白了。
火爆的小茶壶
2022/08/09
1.8K0
如何实现app与微信小程序的二维码聚合?
有意思,小程序还可以一键生成App!
说到小程序,大部分同学的第一反应,可能是微信小程序、支付宝小程序,确实,小程序的概念深入人心,并且已经被约定俗成的绑定到某些互联网公司的 APP 上。
Sb_Coco
2023/03/01
1.2K0
有意思,小程序还可以一键生成App!
重新理解小程序的未来
说到小程序,大部分的读者第一反应,可能是微信小程序、支付宝小程序,确实,小程序的概念深入人心,并且已经被约定俗成的绑定到某些互联网公司的App上。
用户3806669
2022/11/11
4620
重新理解小程序的未来
发现一款好用的小程序IDE开发工具!
在程序开发过程中,有很多可以选择的集成开发环境IDE。市面上IDE工具有很多,今天给大家推荐一款好用的小程序IDE开发工具——FIDE 全称为 FinClip Integrated Development Environment,是一款由 FinClip 面向小程序开发者推出的一款小程序开发者工具。界面与微信小程序的开发工具类似,发现功能挺强大的,界面非常的简洁,上手门槛比较低,简单易上手。最新 FIDE 下载地址:https://www.finclip.com/mop/document/develop/developer/fide-update-log.html
火爆的小茶壶
2022/08/16
1.2K0
发现一款好用的小程序IDE开发工具!
小程序第三方登录的一种方法
最近看到FinClip出了新的解决方案,可以不用另做代码就实现自有小程序用第三方微信登录的问题。我觉得挺不错的,现在分享给大家,也可以给大家做个参考。
pak
2022/06/13
1.2K0
打破超级app垄断,小程序如何具备微信登录体系?
小程序是一种不用下载就能使用的应用,也是一项门槛非常高的创新,经过将近两年的发展,已经构造了新的小程序开发环境和开发者生态。
二山山记
2022/11/16
4220
打破超级app垄断,小程序如何具备微信登录体系?
快速关联微信小程序二维码实现聚合扫码
相信大家在停车场遇到过这样的场景,在停车场入场和出场时,不管我们是用微信还是支付宝,只需台扫同一个二维码,自动识别打开各端小程序,并带入参数停车场id和通道id。
Onegun
2022/06/16
1.7K0
快速关联微信小程序二维码实现聚合扫码
借用FinClip把微信小程序打包生成App
随着小程序时代的全面的来临,加之开发门槛较低,很多企业或个人都拥有了自己的微信小程序,甚至这些大部分企业都没有过自己的App。因为开发难度大,成本高让很多想要开发App的企业望而却步。
Onegun
2022/05/26
1.2K0
借用FinClip把微信小程序打包生成App
已有小程序开发完别着急,还能直接生成App
说到小程序,大部分的读者第一反应,可能是微信小程序、支付宝小程序。确实,以前小程序这种生态只有巨头才玩的起。
芋道源码
2023/08/22
4960
已有小程序开发完别着急,还能直接生成App
在自有App实现小程序的第三方微信授权登录
在微信小程序下,小程序登录功能一般会通过 OpenID 或 UnionID 作为唯一标识,与小程序服务的账号体系进行关联打通,完成用户账户体系的构建与设计。
Onegun
2022/06/07
8880
在自有App实现小程序的第三方微信授权登录
有了小程序,离自营App还远吗?
近年来,不少企业纷纷开始关注小程序的开发。对于用户来说,小程序最大的好处就是能够即点即用,体验便捷。不知你是否发现,小程序经济已经开始制约中小企业的服务与合作。
Lydiasq
2023/01/17
1K0
有了小程序,离自营App还远吗?
微信小程序和App结合的切入点在哪?
为了让开发者可以一次编码,就能够编译为 小程序、 Android、 iOS 应用,实现所谓的 多端开发,去年微信推出了 Donut(多纳) 开发平台(目前是 Beta 版),核心包括:1、开发者可将 小程序 构建成可 独立运行的移动应用;2、也可以将 小程序 构建成运行于 原生应用中的业务模块
Lydiasq
2023/04/24
7760
微信小程序和App结合的切入点在哪?
利用Flutter开发了一个可运行小程序的App
Flutter是Google开源的构建用户界面(UI)工具包,帮助开发者通过一套代码库高效构建多平台精美应用,支持移动、Web、桌面和嵌入式平台。国内很多开发者都将Flutter、Taro、React Native、Weex等列为目前市面上主流的前端框架。
Onegun
2022/07/19
2.6K0
利用Flutter开发了一个可运行小程序的App
推荐阅读
相关推荐
只需4步!带你成功将小程序转为APP
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验