VasSonic深度剖析之iOS端

Sonic是腾讯团队研发的一个轻量级的高性能的Hybrid框架,专注于提升页面首屏加载速度。

这些概念性的东西可以在这里查看,这篇文章我们主要来分析iOS端的表象然后再分析源码。
咋们还是放一张使用Sonic前后效果图,效果还是很明显的

VasSonic深度剖析之iOS端_第1张图片
Soinc效果展示.gif

先来从表象过一下Sonic对请求和响应做了哪些变化

Sonic对请求和响应做了规范约定


VasSonic深度剖析之iOS端_第2张图片
规范约定.png

VasSonic深度剖析之iOS端_第3张图片
cache-offline字段说明.png

所以我们可以看看下面四种场景请求和响应对于没使用sonic都有啥不一样

首次加载

首次加载,因为客户端本地没有数据,所以请求头部会有这样的一些数据

{
    "If-None-Match" = "";
    "accept-Language" = "zh-CN,zh;";
    "accept-diff" = true;
    "template-tag" = "";
    ...
}

从上面的表中可以看到If-None-Match为""表示本地没有缓存的该url的html文件;accept-diff为true表示客户端支持Sonicn规范;template-tag为""表示本地没有缓存该url的template文件。
首次加载服务器返回的响应头部为

{
    "Cache-Offline" = true;
    Etag = 8f53de5336b29c851d3befe20d6f74c8405faa47;
    "template-change" = true;
    "template-tag" = ecad7ac4506804fd64f1fdd6d30d6079d730b50e;
...
}

内容为一个完整的html文件


...
    
    
    
    

当前时间:1503389501

...

Cache-Offline我看了一下服务器的源码,只会返回true和http(后面sonic可能会添加),一般是返回true表示缓存到磁盘并展示返回内容;Etag有值表示本次html内容的唯一标识;template-change为true表示本次请求返回的模版文件和客户端本地缓存的模版文件不一样,首次加载本地就没有模版文件当然不一样咯;template-tag有值模版文件的唯一标识。
这时候你可以看看客户端本地对于该url缓存了4个文件,名字用url通过md5加密


VasSonic深度剖析之iOS端_第4张图片
客户端本地缓存文件.png

cfg文件内容为



    Etag
    486fde9e2343fcf44563fea7d30570e695aa31ad
    template-tag
    ecad7ac4506804fd64f1fdd6d30d6079d730b50e
...


很熟悉吧,就是把服务器头部返回的保存起来了;data文件内容为



    {data1}
    
    

当前时间:1503389501

这个就是本次请求服务器返回的动态变化的值,也就是你前端标记了sonicdiff的标签在本次响应中的值;html就不需要说了,就是一个完整的html文件


...
    
    
    
    

当前时间:1503389501

...

temp是sonic根据前端html文件抽离出来的模版


...
    
    
    {data1}
    
   ...

这样我们的首次加载就完成了,服务器返回完整的html文件、客户端本地缓存本次请求相关的文件

数据变化

第二次打开网页的时候你能马上看到页面内容,是因为Sonic加载了本地对该url缓存的html文件;请求头部变成了

{
    "If-None-Match" = bbf85d933f2d42554af04b618f0875d958fa39fb;
    "accept-diff" = true;
    "template-tag" = ecad7ac4506804fd64f1fdd6d30d6079d730b50e;
...
}

相对于首次加载,为""的key都有了值,是因为在本地读取了sonic对于该url缓存的cfg文件;服务器响应头部变成了

{
    "Cache-Offline" = true;
    Etag = c70b4b4aa53295c1c6320ec1ec1dee809332669f;
    "template-change" = false;
    "template-tag" = ecad7ac4506804fd64f1fdd6d30d6079d730b50e;
...
}

你可以看到template-tag和请求时一样,说明模版没有变化,template-change为false是个意思;Etag和请求时If-None-Match不同说明页面内容有变化;这时候服务器返回了如下内容

{
    "data": {
        "{title}": "Sonic Demo<\/title>",
        "{data1}": "<!--sonicdiff-data1-->\n    <h2>\u5f53\u524d\u65f6\u95f4\uff1a1503390256<\/h2>\n    <!--sonicdiff-data1-end-->"
    },
    "template-tag": "ecad7ac4506804fd64f1fdd6d30d6079d730b50e",
    "html-sha1": "af3f21e49dcf76ae538095a18d82bf10692db06e",
    "diff": ""
}
</code></pre> 
 <p>是不是很惊讶,sonic做了处理,对于模版没有变化的页面只返回了动态数据变化的部分;没错,这大大的降低了流量,也加快了请求;这时候你再看看本地的缓存文件,temp文件内容不变,其他三个文件内容更新了(自己理解一下)<br> 数据更新请求完了,客户端先加载本地缓存的html文件、服务器返回变化的部分、客户端本地更新必要的文件</p> 
 <h4>模版变化</h4> 
 <p>模版变化其实和首次加载只有一点点区别,就是客户端会先加载本地缓存的html文件;服务器响应依然会返回整个html文件(其实我觉得这个sonic可以再优化优化,估计是遇到了什么困难),客户端本地会更新所有四个文件</p> 
 <h4>完全缓存</h4> 
 <p>这个很好理解了,客户端会先加载本地缓存的html文件,服务器返回状态码304表示内容和客户端一摸一样,客户端只需要更新cfg文件</p> 
 <h2>再来从源码发现Sonic对请求和响应做了哪些变化</h2> 
 <p>依然老规矩,我们会按照官方说的首次加载、数据更新和完全缓存三种情况(官方说四种但是首次加载和模版更新客户端逻辑一样)来看一下客户端会经过哪些源码。</p> 
 <h6>注:阅读此部分请你先下载我写的超简单支持Sonic的PHP服务器和iOS客户端,然后仔细阅读NSURLProtocol简单实用、Mac搭建PHP本地环境在本地部署好环境,最后结合Sonic之iOS端实现原理一起看本文章;理所当然的本文我会按照这两份源码来讲解;iOS和Web交互中间件使用WebViewJavascriptBridge。</h6> 
 <p>下载服务器代码后在index.php中注释掉这一行关闭模版更新(首次加载和模版更新客户端逻辑一样)</p> 
 <pre><code>...
//每次重新打开此界面都改变此值,模拟模版变化
$templateFlag = $_COOKIE['templateFlag'];
//模拟模版更新
// setcookie('templateFlag',!$templateFlag);
...
</code></pre> 
 <p>如果你本地php服务器环境搭建完成了,你浏览器中输入类似http://localhost/sonic-php/sample/index.php地址就会看到下面的页面,记得把客户端中的地址也改改</p> 
 <div class="image-package"> 
  <div class="image-container" style="max-width: 700px; max-height: 414px;"> 
   <div class="image-view"> 
    <a href="http://img.e-com-net.com/image/info10/802f6ea1562a42fe8b667725d9cb4a26.jpg" target="_blank"><img src="http://img.e-com-net.com/image/info10/802f6ea1562a42fe8b667725d9cb4a26.jpg" width="650" height="202" alt="VasSonic深度剖析之iOS端_第5张图片" style="border:1px solid black;"></a> 
   </div> 
  </div> 
  <div class="image-caption">
    前端界面.png 
  </div> 
 </div> 
 <p></p> 
 <h2>首次加载</h2> 
 <h6>初始化</h6> 
 <p>我们看看WebViewController.m的init方法:</p> 
 <pre><code>...
[[SonicClient sharedClient] createSessionWithUrl:@"http://localhost/sonic-php/sample/index.php" withWebDelegate:self];
</code></pre> 
 <p>还记得这里说的客户端并行请求数据吗?说的就是在ViewController初始化的时候Sonic就开始向服务器请求数据而不用等到webView调用loadRequest;我们进入SonicClient,sharedClient就是获取SonicClient的单例对象,init时调了[self setupClient]</p> 
 <pre><code>- (void)setupClient {
    self.lock = [NSRecursiveLock new];
    self.tasks = [NSMutableDictionary dictionary];
    self.ipDomains = [NSMutableDictionary dictionary];
}
</code></pre> 
 <p>其中lock采用NSRecursiveLock防止死锁;tasks用于存放当前url和session对应关系,因为你可以同时存在几个带webView的vc;ipDomains存放域名和地址的对应关系,举个例子,我们的演示demo地址是http://localhost/sonic-php/sample/index.php,如果你做了下面的操作</p> 
 <pre><code>[[SonicClient sharedClient] addDomain:@"localhost" withIpAddress:@"127.0.0.1"];
</code></pre> 
 <p>那么发起该url请求时SonicSession的serverIP值就会被设置为127.0.0.1并且请求头部也会加上一些参数(其实我提了一个issues并给腾讯团队提了pull request),最后发起请求的地址也就成了http://127.0.0.1/sonic-php/sample/index.php<br> 现在来看看</p> 
 <pre><code>if ([[SonicCache shareCache] isServerDisableSonic:sonicSessionID(url)]) 
        return;
</code></pre> 
 <p>[SonicCache shareCache]同样的是获取SonicCache的单例对象,在init的时[self setupInit]</p> 
 <pre><code>- (void)setupInit
{
    self.maxCacheCount = 3;
    self.lock = [NSRecursiveLock new];
    self.memoryCache = [NSMutableDictionary dictionaryWithCapacity:self.maxCacheCount];
    self.recentlyUsedKey = [NSMutableArray arrayWithCapacity:self.maxCacheCount];
    [self setupCacheDirectory];
    [self setupCacheOfflineTimeCfgDict];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(memoryWarningClearCache) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
</code></pre> 
 <p>maxCacheCount表示内存缓存中保留的SonicCacheItem个数;memoryCache表示内存缓存的SonicCacheItem;recentlyUsedKey相当于做了一个先进先出的操作,如果当前内存缓存数量超过了maxCacheCount那么就删除最先加入的;setupCacheDirectory是创建缓存目录;setupCacheOfflineTimeCfgDict是服务器容灾措施,如果服务器访问量太大,会给一部分客户响应头cache-offline值设置为http让这部分客户端暂时不能对于特定url访问服务器(offlineCacheTimeCfg会对单个url配置限制时间),本地如果有缓存的该配置文件就填充offlineCacheTimeCfg属性;最后一句话是如果应用内存紧张了就删除内存缓存的数据。<br> sonicSessionID(url)是Sonic用md5加密并标示标示每个url的</p> 
 <pre><code>if ([[SonicClient sharedClient].currentUserUniq length] > 0) 
        return stringFromMD5([NSString stringWithFormat:@"%@_%@",[SonicClient sharedClient].currentUserUniq,sonicUrl(url)]);
    else
        return stringFromMD5([NSString stringWithFormat:@"%@",sonicUrl(url)]);
</code></pre> 
 <p>Sonic给我们设一个属性currentUserUniq,让我们可以设置每次进入网页的身份(你的app当然可以退出再登陆其他账号),Sonic好分用户对应文件夹村存放缓存文件。<br> 所以我们首次加载的时候[[SonicCache shareCache] isServerDisableSonic:sonicSessionID(url)肯定是返回NO的,因为我们没有发起过请求,本地自然没有服务器对于容灾的配置文件,SonicCache的offlineCacheTimeCfg自然也是空的。然后我们走下面的代码</p> 
 <pre><code>[self.lock lock];
SonicSession *existSession = self.tasks[sonicSessionID(url)];
//这里都会为nil,因为我们在控制器返回时从self.tasks中remove了该会话
if (existSession && existSession.delegate != nil) {
    //session can only owned by one delegate
    [self.lock unlock];
    return;
}
</code></pre> 
 <p>加锁进行后面的操作,找到tasks中该url是否已经有一个对应的回话了,这里我们要看看WebViewController.m的dealloc</p> 
 <pre><code>[[SonicClient sharedClient] removeSessionWithWebDelegate:self];
</code></pre> 
 <p>也就是说如果你的控制器能正常释放且你应用不会打开两个相同url界面的话, 这里的existSession肯定是nil的,我们不分析两个界面同时打开相同url</p> 
 <pre><code>NSURL *cUrl = [NSURL URLWithString:url];
NSString *serverIP = [self.ipDomains objectForKey:cUrl.host]?:@"";
existSession = [[SonicSession alloc] initWithUrl:url withServerIP:serverIP withWebDelegate:aWebDelegate];
</code></pre> 
 <p>这里就是创建会话了,serverIP就是该url在self.ipDomains中对应的值,这个前面已经提过了;我们看看SonicSession初始化都做了什么</p> 
 <pre><code>...
_sessionID = [sonicSessionID(aUrl) copy];
[self setupData];
- (void)setupData {
    SonicCacheItem *cacheItem = [[SonicCache shareCache] cacheForSession:_sessionID];
    ...
}
</code></pre> 
 <p>[[SonicCache shareCache] cacheForSession:_sessionID]是获取该会话id在本地对应的缓存文件,我们看看cacheForSession</p> 
 <pre><code>SonicCacheItem *cacheItem = nil;
cacheItem = self.memoryCache[sessionID];
if (!cacheItem) {
    cacheItem = [[SonicCacheItem alloc] initWithSessionID:sessionID];
    [self memoryCacheItem:cacheItem];
    [self setupCacheItemFromFile:cacheItem];
    [cacheItem release];
}
...
</code></pre> 
 <p>self.memoryCache[sessionID]表示先从内存缓存中找到该会话是否有保存的cacheItem,如果没有就用sessionID初始化一个,memoryCacheItem表示记录到内存缓存中</p> 
 <pre><code>- (void)memoryCacheItem:(SonicCacheItem *)cacheItem {
    //保存到内存缓存中
    [self.memoryCache setObject:cacheItem forKey:cacheItem.sessionID];
    //找到该cache对应的下标
    NSUInteger index = [self.recentlyUsedKey indexOfObject:cacheItem.sessionID];
    if (index != NSNotFound) {
        [self.recentlyUsedKey removeObjectAtIndex:index];
    }
    //并把该cache放到第一个,用来记录最近使用的cache
    [self.recentlyUsedKey insertObject:cacheItem.sessionID atIndex:0];
    //如果需要缓存的cache个数大于限制
    if (self.recentlyUsedKey.count > self.maxCacheCount) {
        //我们就去掉最后一个,也就是最先加入的
        NSString *lastUsedKey = [self.recentlyUsedKey lastObject];
        [self.memoryCache removeObjectForKey:lastUsedKey];
        [self.recentlyUsedKey removeObject:lastUsedKey];
    }
}
</code></pre> 
 <p>再来看看[self setupCacheItemFromFile:cacheItem]</p> 
 <pre><code>- (void)setupCacheItemFromFile:(SonicCacheItem *)item {
    //检查该item是否有html data config template四个文件
    //如果有任何一个文件不存在,就删除所有文件
    if (![self isAllCacheExist:item.sessionID]) {
        [self removeFileCacheOnly:item.sessionID];
        return;
    }
    //如果四个文件都存在,就取出里面的数据
    NSData *htmlData = [NSData dataWithContentsOfFile:[self filePathWithType:SonicCacheTypeHtml sessionID:item.sessionID]];
    NSDictionary *config = [NSDictionary dictionaryWithContentsOfFile:[self filePathWithType:SonicCacheTypeConfig sessionID:item.sessionID]];
    NSString *sha1 = config[kSonicSha1];
    NSString *htmlSha1 = getDataSha1(htmlData);
    //如果检测出来内容的sha值不是cfg中保存的值,说明数据发生了错误,需要删除文件
    if (![sha1 isEqualToString:htmlSha1]) {
        [self removeFileCacheOnly:item.sessionID];
    }else{
        //如果所有数据正常,就赋值给item
        item.htmlData = htmlData;
        item.config = config;
    }
}
</code></pre> 
 <p>这里的意思很明确了,如果本地有该url的缓存文件就读取放到cacheItem中,如果没有那么本地啥文件都没有;接下来我们接着看SonicSession的setupData剩下的部分:</p> 
 <pre><code>//是不是第一次加载,通过item的htmlData数据来判断
self.isFirstLoad = cacheItem.hasLocalCache;
//如果本地有该会话缓存的数据就给本session赋值
if (!cacheItem.hasLocalCache) {
   self.cacheFileData = cacheItem.htmlData;
   self.cacheConfigHeaders = cacheItem.config;
   self.cacheResponseHeaders = cacheItem.cacheResponseHeaders;
   self.localRefreshTime = cacheItem.lastRefreshTime;
}
[self setupConfigRequestHeaders];
</code></pre> 
 <p>setupConfigRequestHeaders里面的内容就不说了,就是设置请求的头部,里面有很多Sonic扩展的key可以在这里看到。<br> 接下来继续看SonicClient中createSessionWithUrl::剩下的部分</p> 
 <pre><code>[existSession setCompletionCallback:^(NSString *sessionID){
     [existSession cancel];
     [self.tasks removeObjectForKey:sessionID];
}];
[self.tasks setObject:existSession forKey:existSession.sessionID];
[existSession start];
</code></pre> 
 <p>设置会话完成时从tasks中删除该会话,把当前会话加到tasks中,然后开始start</p> 
 <pre><code>dispatchToMain(^{
//通知控制器(因为self.delegate已经指向了打开webView的控制器),我要对该url进行请求了
if (self.delegate && [self.delegate respondsToSelector:@selector(sessionWillRequest:)]) {
  [self.delegate sessionWillRequest:self];
}
  //如果控制器有设置cookies,在这里同步添加到请求
  [self syncCookies];
});
//异步执行请求
[self requestStartInOperation];
</code></pre> 
 <p>requestStartInOperation就是异步请求数据(按照程序执行的先后顺序,后面讲这个),看到这里我们要回过头来看WebViewController.m的viewDidLoad了</p> 
 <h6>webView发起请求</h6> 
 <pre><code>...
if ([[SonicClient sharedClient] sessionWithWebDelegate:self]) 
  [self.webView loadRequest:sonicWebRequest(request)];
...
- (SonicSession *)sessionWithWebDelegate:(id<SonicSessionDelegate>)aWebDelegate {
    //检测aWebDelegate是否实现了SonicSessionDelegate协议
    if (!ValidateSessionDelegate(aWebDelegate)) 
        return nil;
    SonicSession *findSession = nil;
    [self.lock lock];
    //从tasks中找到代理是aWebDelegate的会话
    for (SonicSession *session in self.tasks.allValues) {
        if (session.delegate == aWebDelegate) {
             findSession = session;
             break;
       }
    }
    [self.lock unlock];
    return findSession;
}
</code></pre> 
 <p>如果findSession为nil有下面两种情况</p> 
 <pre><code>1: aWebDelegate没有实现SonicSessionDelegate协议
2:tasks中没有delegate为aWebDelegate的会话
</code></pre> 
 <p>第二种情况往往是控制器没有delloc导致的,如果你也遇到了就需要好好的检查一下了。代码走到这里后我们就要好好看一下sonicWebRequest(request)了</p> 
 <pre><code>#define SonicHeaderValueWebviewLoad   @"__SONIC_HEADER_VALUE_WEBVIEW_LOAD__"

NSMutableURLRequest *request = [[originRequest mutableCopy]autorelease];
[request setValue:SonicHeaderValueWebviewLoad forHTTPHeaderField:SonicHeaderKeyLoadType];
[request setValue:sonicSessionID(request.URL.absoluteString) forHTTPHeaderField:SonicHeaderKeySessionID];
######拦截webView请求
</code></pre> 
 <p>这里我们要注意SonicHeaderValueWebviewLoad宏,只有请求头部设置了此值webView的loadRequest才会被SonicURLProtocol所拦截(前面我让你看过NSURLProtocol文章)。所以接下来我们当然是看SonicURLProtocol.m的startLoading方法咯</p> 
 <pre><code>NSThread *currentThread = [NSThread currentThread];
//得到当前会话的id
NSString *sessionID = [self.request valueForHTTPHeaderField:SonicHeaderKeySessionID];
//让SonicClient接管此请求
[[SonicClient sharedClient] registerURLProtocolCallBackWithSessionID:sessionID completion:^(NSDictionary *param) {
   //SonicClient请求返回的数据通过callClientActionWithParams处理
   [self performSelector:@selector(callClientActionWithParams:) onThread:currentThread withObject:param waitUntilDone:NO];
}];
</code></pre> 
 <p>callClientActionWithParams写法基本是固定的,就是在各个请求阶段取出SonicClient回调的数据传输给self.client去继续渲染界面。<br> 看到这里我们大概明白了,webView的loadRequest被SonicClient接管了,我们现在来看看SonicClient的registerURLProtocolCallBackWithSessionID,这个是重点</p> 
 <pre><code>...
[session preloadRequestActionsWithProtocolCallBack:protocolCallBack];
- (void)preloadRequestActionsWithProtocolCallBack:(SonicURLProtocolCallBack)protocolCallBack {
    dispatch_block_t opBlock = ^{
        self.protocolCallBack = protocolCallBack;
        //如果不是第一次请求该url,是不是第一次是根据本地是否有该url缓存文件判断的
        //或者模版更新sonic让webView重新loadRequest或者如果当前数据请求Sonic已经完成了(因为self.isDataUpdated只会在请求完成且没有错误才被设置),意思就是说我在webView在调用loadRequest前Sonic就已经获取完了整个请求数据
        if (self.isDataUpdated || !self.isFirstLoad) {
            //如果protocolCallBack不为空
            if (protocolCallBack)
                //直接就可以告诉SonicURLProtocol请求完成并返回数据,展示界面
                [self dispatchProtocolActions:[self cacheFileActions]];
            //如果模版更新sonic让webView重新loadRequest或者请求已经完成了,设置sonicStatus的状态为完全缓存;如果Sonic请求完数据会直接存到本地的,状态说是完全缓存也是理所当然的
            if (self.isDataUpdated) 
                self.sonicStatusFinalCode = SonicStatusCodeAllCached;
        } else {
            //如果是第一次加载数据且Sonic请求还没有完成或者请求出错了,就把Sonic已经完成的阶段回调给SonicURLProtocol,self.isCompletion只会在请求发生错误或者请求完成时设置
            //还没有完成的阶段,后面会陆续回调给SonicURLProtocol
            if (self.isFirstLoad) 
                [self dispatchProtocolActions:[self preloadRequestActions]];
        }
    };
    //把block丢给SonicSessionQueue,先后顺序执行
    dispatchToSonicSessionQueue(opBlock);
}
</code></pre> 
 <p>上面的代码就是Sonic的核心了,为什么第二次访问url会那么快,是因为第二次是直接取对应的cacheItem让webView展示。在这里我们是首次加载,所以会走else代码块,为了方便讲解,我们假设现在Sonic一个请求阶段都没有完成,那么这时候我们就要回到刚才SonicSession的requestStartInOperation了</p> 
 <pre><code>Class customRequest = [self canCustomRequest];
if (!customRequest) {
   //If there no custom request ,then use the default
   customRequest = [SonicConnection class];
}
...
//用户如果有自定义的请求连接方式(比如:用户继承SonicConnection,用AFNetWorking发起请求等)
- (Class)canCustomRequest {
    Class findDestClass = nil;
    for (NSInteger index = sonicRequestClassArray.count - 1; index >= 0; index--) {
        Class itemClass = sonicRequestClassArray[index];
        //构造一个此类的canInitWithRequest方法,调用
        NSMethodSignature *sign = [itemClass methodSignatureForSelector:@selector(canInitWithRequest:)];
        NSInvocation *invoke = [NSInvocation invocationWithMethodSignature:sign];
        invoke.target = itemClass;
        NSURLRequest *argRequest = self.request;
        [invoke setArgument:&argRequest atIndex:2];
        invoke.selector = @selector(canInitWithRequest:);
        [invoke invoke];
        BOOL canCustomRequest;
        //如果有能处理该requeset的(用户继承SonicConnection,canInitWithRequest返回YES)
        [invoke getReturnValue:&canCustomRequest];
        if (canCustomRequest) {
            findDestClass = itemClass;
            break;
        }
    }
    return findDestClass;
}
</code></pre> 
 <p>这里就比较有意思了,SonicSDK默认使用的SonicConnection类作为连接,你可以看到SonicConnection内部是用NSURLSessionDataTask发起的请求,用户可以自己继承SonicConnection用比如AFNetWorking等发起请求;所以[self canCustomRequest]方法就是获取用户自定义的请求方式是否有能响应该request的,也就是canInitWithRequest:返回YES的,如果没有就用初始化SonicConnection。接下来当然是初始化请求,然后建立请求咯</p> 
 <pre><code>...
//初始化请求类
SonicConnection *cRequest = [[customRequest alloc]initWithRequest:self.request];
//把请求类保存到自己mCustomConnection属性中
self.mCustomConnection = cRequest;
[cRequest release];
//设置请求类发起请求的各个阶段需要通知到自己处理
//然后处理结果会通过self.protocolCallBack给到SonicURLProtocol
self.mCustomConnection.session = self;
//开始加载请求
[self.mCustomConnection startLoading];
</code></pre> 
 <p>因为我们是源码讲解,所以我们假设发起请求的类是SonicConnection;其实SonicConnection代码没啥看不懂的,就是数据请求哈,我们只讲重要的请求阶段完成后发生了啥(特别需要SonicSession的一些属性设置)</p> 
 <h6>收到服务器响应</h6> 
 <p>首先当然是收到服务器响应阶段了</p> 
 <pre><code>[self.session session:self.session didRecieveResponse:(NSHTTPURLResponse *)response];
</code></pre> 
 <p>刚才说了SonicConnection的session就是SonicSession,我们会到SonicSession中</p> 
 <pre><code>- (void)session:(SonicSession *)session didRecieveResponse:(NSHTTPURLResponse *)response {
...
self.response = response;
self.cacheResponseHeaders = response.allHeaderFields;
if (self.isFirstLoad)
    [self firstLoadRecieveResponse:response];
...
}
- (void)firstLoadRecieveResponse:(NSHTTPURLResponse *)response {
    [self dispatchProtocolAction:SonicURLProtocolActionRecvResponse param:response];
}
//回传给SonicURLProtocol
- (void)dispatchProtocolAction:(SonicURLProtocolAction)action param:(NSObject *)param {
    NSDictionary *actionParam = [self protocolActionItem:action param:param];
    if (self.protocolCallBack) {
        self.protocolCallBack(actionParam);
    }
}
</code></pre> 
 <p>把响应和头部存在SonicSession属性中,如果是第一次加载,回传给SonicURLProtocol。前面说了SonicURLProtocol中</p> 
 <pre><code>- (void)callClientActionWithParams:(NSDictionary *)params {
...
}
</code></pre> 
 <p>方法是固定的,没啥特别的;服务器返回的code有以下几种情况</p> 
 <pre><code>1000:第一次请求,及本地没有缓存文件,此时服务器会返回整个前端内容
2000:非第一次请求,本地和服务器内容模版有变化,此时服务器会返回整个前端内容
304:非第一次请求,本地可服务器内容完全一样,此时服务器不返回前端任何内容
503:服务器要求客户端不用Sonic重新加载请求
200:非第一次请求,本地和服务器内容模版无变化,但动态数据有变化,此时服务器会返回前端变化的数据
注:至于服务器是怎么判断返回什么的,你就去看看源码咯
</code></pre> 
 <p>所以我们继续走SonicConnection中请求下一个阶段</p> 
 <h6>收到服务器数据</h6> 
 <pre><code>[self.session session:self.session didLoadData:data]
//收到请求返回的数据
- (void)session:(SonicSession *)session didLoadData:(NSData *)data {
    dispatch_block_t opBlock = ^{
        if (!self.responseData)
            self.responseData = [NSMutableData data];
       //如果有数据,就不断的添加到self.responseData中
        if (data) {
            NSData *copyData = [data copy];
            [self.responseData appendData:data];
            [copyData release];
            //如果是第一次加载,执行firstLoadDidLoadData
            if (self.isFirstLoad)
                [self firstLoadDidLoadData:data];
        }
    };
    dispatchToSonicSessionQueue(opBlock);
}
- (void)firstLoadDidLoadData:(NSData *)data
{
   //回传给SonicURLProtocol
    [self dispatchProtocolAction:SonicURLProtocolActionLoadData param:data];
}
</code></pre> 
 <p>收到服务器响应阶段也是如此简单,就是把返回的数据存在SonicSession的responseData中;接下来在SonicConnection中可能会走下面两个方法</p> 
 <pre><code>//请求完成
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
...
}
//请求变无效
- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(nullable NSError *)error {
...
}
</code></pre> 
 <h6>请求完成</h6> 
 <pre><code>//如果发生错误,执行此
if (error)
    [self.session session:self.session didFaild:error];
else//如果未发生错误,执行此
    [self.session sessionDidFinish:self.session];
</code></pre> 
 <p>如果发生错误,走SonicSession的下面代码</p> 
 <pre><code>//请求完成,且发生错误
- (void)session:(SonicSession *)session didFaild:(NSError *)error {
    dispatch_block_t opBlock = ^{
        //把错误信息记录到SonicSession的error中
        self.error = error;
        //标记请求已经完成
        self.isCompletion = YES;
        //请求错误但是服务器返回304表示完全缓存,也就是说服务器本次请求的数据和客户端保存的数据完全一样
        if (self.response.statusCode == 304) {
            //是不是第一次加载
            if (self.isFirstLoad)
                [self firstLoadDidFinish];
            else
                [self updateDidSuccess];
        }else{//请求错误且服务器没返回304,说明真的出错了
            if (self.isFirstLoad)
                [self firstLoadDidFaild:error];
            else
                [self updateDidFaild];
        }
    };
    dispatchToSonicSessionQueue(opBlock);
}
...
</code></pre> 
 <p>正常的逻辑我们会走到[self firstLoadDidFaild:error],因为本地没有缓存自然不会返回304</p> 
 <pre><code>//第一次加载请求,并且失败了
- (void)firstLoadDidFaild:(NSError *)error {
    //传给SonicURLProtocol
    [self dispatchProtocolAction:SonicURLProtocolActionDidFaild param:error];
    //如果self.completionCallback有值,就执行
    [self checkAutoCompletionAction];
}
- (void)checkAutoCompletionAction {
    //主线程依次执行回调
    dispatchToMain(^{
        if (!self.delegate) 
            if (self.completionCallback) 
                self.completionCallback(self.sessionID);
    });
}
</code></pre> 
 <p>这里的completionCallback还记得SonicClient的createSessionWithUrl::里面吗</p> 
 <pre><code>[existSession setCompletionCallback:^(NSString *sessionID){
     [existSession cancel];
     [self.tasks removeObjectForKey:sessionID];
}];
</code></pre> 
 <p>其实就是如果第一次请求发生错误,就取消该url的会话并从tasks中删除,如果第一次请求真的失败了,那么整个代码流程也就走完了。接下来我们来分析请求完成,并且没有失败的情况,会走SonicSession的sessionDidFinish:</p> 
 <pre><code>self.isCompletion = YES;
if (self.isFirstLoad) 
   [self firstLoadDidFinish];
else
   [self updateDidSuccess];
//第一次加载请求完成,且没有错误
- (void)firstLoadDidFinish {
    //通知SonicURLProtocol请求完成
    [self dispatchProtocolAction:SonicURLProtocolActionDidFinish param:nil];
    if (![self isCompletionWithOutError]) {
        return;
    }
    //服务器返回状态
    switch (self.response.statusCode) {
        //如果本地缓存内容和服务器内容模版无变化,但是动态数据有变化
        case 200: {
            //如果服务器设置了客户端的行为,也就是有cache-offline字段,这个
            //服务器一般都有设置,并且值为true
            if ([self isSonicResponse]) {
                //获取服务器对于该客户端指派的行为
                //true:缓存到磁盘并展示返回内容
                //false:展示返回内容,无需缓存到磁盘
                //store:缓存到磁盘,如果已经加载缓存,则下次加载,否则展示返回内容
                //http:容灾字段,如果http表示终端六个小时之内不会采用sonic请求该URL
                NSString *policy = [self responseHeaderValueByIgnoreCaseKey:SonicHeaderKeyCacheOffline];
                //把服务器返回的数据存在cacheFileData中,后面会进行操作
                self.cacheFileData = self.responseData;
                //如果值为http,表示终端六个小时之内不会采用sonic请求该URL
                if ([policy isEqualToString:SonicHeaderValueCacheOfflineDisable]) {
                    //这时候就需要把这些配置写入文件了
                    [[SonicCache shareCache] saveServerDisabeSonicTimeNow:self.sessionID];
                    self.isDataUpdated = YES;
                    break;
                }
                //如果值为true、false、store,表示需要缓存到磁盘
                if ([policy isEqualToString:SonicHeaderValueCacheOfflineStoreRefresh] || [policy isEqualToString:SonicHeaderValueCacheOfflineStore] || [policy isEqualToString:SonicHeaderValueCacheOfflineRefresh]) {
                    //把本次请求的内容分html、data、cfg、template存放到本地缓存目录
                    SonicCacheItem *cacheItem = [[SonicCache shareCache] saveFirstWithHtmlData:self.responseData withResponseHeaders:self.response.allHeaderFields withUrl:self.url];
                    if (cacheItem) {
                        self.localRefreshTime = cacheItem.lastRefreshTime;
                        self.sonicStatusCode = SonicStatusCodeFirstLoad;
                        self.sonicStatusFinalCode = SonicStatusCodeFirstLoad;
                    }
                    //如果值为false,需要删除本地缓存
                    if ([policy isEqualToString:SonicHeaderValueCacheOfflineRefresh]) 
                        [[SonicCache shareCache] removeCacheBySessionID:self.sessionID];
                    //去掉服务器对客户端的访问限制,万一之前设置了,现在没设置,就可以在这里删除
                    [[SonicCache shareCache] removeServerDisableSonic:self.sessionID];
                }
            }else{
                self.cacheFileData = self.responseData;
            }
            //设置数据已经更新到本地标志
            self.isDataUpdated = YES;
        }
            break;
        case 503://要求客户端
            [self webViewRequireloadNormalRequest];
            break;
        default:
            break;
    }
    //如果设置有completionCallback,挨个回调
    [self checkAutoCompletionAction];
}
</code></pre> 
 <p>第一次成功请求会走200代码块,其实demo中注释我已经写的很多了,相信你也能看懂;那么如果第一次请求完成且没有错误,该走的代码也就走完了,这时候你去缓存目录看看就会多出那四个对应的文件</p> 
 <h6>请求变无效</h6> 
 <p>其实和请求完成发生错误走的是同一个方法,就是通知SonicURLProtocol发生错误了,如果self.completionCallback有值,就挨个回调<br> 文章写到这里,其实大部分的代码我们都已经讲过了,我们注意到请求完成且没有错误响应数据才会写入文件,我们假设我们请求完成且没有错误且本地所有文件正常创建,接下来分析第二次请求模版不变数据更新的情况</p> 
 <h2>数据更新</h2> 
 <p>其实前面的步骤都是差不多的,从SonicClient的createSessionWithUrl开始有点不一样</p> 
 <pre><code>...
existSession = [[SonicSession alloc] initWithUrl:url withServerIP:serverIP withWebDelegate:aWebDelegate];
...
[self setupData];
- (void)setupData
{
    //得到该会话的缓存数据 html template data cconfig
    SonicCacheItem *cacheItem = [[SonicCache shareCache] cacheForSession:_sessionID];
    //是不是第一次加载
    self.isFirstLoad = cacheItem.hasLocalCache;
    //如果本地有该会话缓存的数据 如果有缓存的数据,就给self赋值,添加请求头部时有用
    if (!cacheItem.hasLocalCache) {
        self.cacheFileData = cacheItem.htmlData;
        self.cacheConfigHeaders = cacheItem.config;
        self.cacheResponseHeaders = cacheItem.cacheResponseHeaders;
        self.localRefreshTime = cacheItem.lastRefreshTime;
   }
}
</code></pre> 
 <p>因为本地有缓存文件,所以这时候的cacheItem各项其实是有值的,self.isFirstLoad自然也是NO;<br> 同样在SonicURLProtocol中startLoading中</p> 
 <pre><code>[[SonicClient sharedClient] registerURLProtocolCallBackWithSessionID:sessionID completion:^(NSDictionary *param) {
...
}];
- (void)preloadRequestActionsWithProtocolCallBack:(SonicURLProtocolCallBack)protocolCallBack {
...
if (self.isDataUpdated || !self.isFirstLoad) {
     //直接就可以告诉SonicURLProtocol请求完成并返回数据,展示界面
     [self dispatchProtocolActions:[self cacheFileActions]];
...
}
}
</code></pre> 
 <p>这时候if是为真的,Sonic直接就把缓存的数据丢给SonicURLProtocol了,webView自然就直接显示缓存文件了,这当然是秒开咯!然后SonicConnection还是会继续发起请求的,我们直接看请求完成且没有错误时应该执行什么吧</p> 
 <pre><code>if (error)
     [self.session session:self.session didFaild:error];
else
     [self.session sessionDidFinish:self.session];
- (void)sessionDidFinish:(SonicSession *)session {
    dispatch_block_t opBlock = ^{
        //标记请求完成
        self.isCompletion = YES;
        if (self.isFirstLoad) 
            [self firstLoadDidFinish];
        else
            [self updateDidSuccess];
    };
    dispatchToSonicSessionQueue(opBlock);
}
</code></pre> 
 <p>这时候会走 [self updateDidSuccess]方法,这个方法我们要好好的分析一下了</p> 
 <pre><code>- (void)updateDidSuccess {
    if (![self isCompletionWithOutError])
        return;
    switch (self.response.statusCode) {
            //服务器返回304表示完全缓存
        case 304: {
            //记录下来 就可以直接返回了,因为完全缓存客户端并不需要做什么
            self.sonicStatusCode = SonicStatusCodeAllCached;
            self.sonicStatusFinalCode = SonicStatusCodeAllCached;
        }
            break;
        //200表示模版有更新、或者模版未更新但动态数据有更新
        case 200: {
            //如果服务器响应中没有设置cache-offline,直接返回;说明此请求不满足Sonic规范
            if (![self isSonicResponse])
                break;
            //如果是模版更新
            if ([self isTemplateChange]) {
                self.cacheFileData = self.responseData;
                [self dealWithTemplateChange];
            }else{//如果是模版未变化但动态数据更新
                [self dealWithDataUpdate];
            }
            //获取服务器对于该客户端指派的行为
            //true:缓存到磁盘并展示返回内容
            //false:展示返回内容,无需缓存到磁盘
            //store:缓存到磁盘,如果已经加载缓存,则下次加载,否则展示返回内容
            //http:容灾字段,如果http表示终端六个小时之内不会采用sonic请求该URL
            NSString *policy = [self responseHeaderValueByIgnoreCaseKey:SonicHeaderKeyCacheOffline];
            //store true false需要删除限制
            if ([policy isEqualToString:SonicHeaderValueCacheOfflineStore] || [policy isEqualToString:SonicHeaderValueCacheOfflineStoreRefresh] || [policy isEqualToString:SonicHeaderValueCacheOfflineRefresh]) {
                //去掉服务器对客户端的访问限制,万一之前设置了,现在没设置,就可以在这里删除
                [[SonicCache shareCache] removeServerDisableSonic:self.sessionID];
            }
            //false 删除该sessionID的内存缓存以及本地缓存
            if ([policy isEqualToString:SonicHeaderValueCacheOfflineRefresh])
                [[SonicCache shareCache] removeCacheBySessionID:self.sessionID];
            //http 需要保存限制
            if ([policy isEqualToString:SonicHeaderValueCacheOfflineDisable])
                [[SonicCache shareCache] saveServerDisabeSonicTimeNow:self.sessionID];
        }
            break;
        default:
            break;
    }
    //用callBack告诉网页数据变化了
    //如果有设置[[SonicClient sharedClient] sonicUpdateDiffDataByWebDelegate:self completion:^(NSDictionary *result) {...}会自动触发
    if (self.webviewCallBack) {
        NSDictionary *resultDict = [self sonicDiffResult];
        if (resultDict) 
            self.webviewCallBack(resultDict);
    }
    [self checkAutoCompletionAction];
}
//如果是模版更新
- (void)dealWithTemplateChange{
    //重新把响应写入到本地缓存文件
    SonicCacheItem *cacheItem = [[SonicCache shareCache] saveFirstWithHtmlData:self.responseData withResponseHeaders:self.response.allHeaderFields withUrl:self.url];
    //如果写入成功,给当前会话设置一些值
    if (cacheItem) {
        self.sonicStatusCode = SonicStatusCodeTemplateUpdate;
        self.sonicStatusFinalCode = SonicStatusCodeTemplateUpdate;
        self.localRefreshTime = cacheItem.lastRefreshTime;
        self.cacheFileData = self.responseData;
        self.cacheResponseHeaders = cacheItem.cacheResponseHeaders;
        //标记数据已经更新到本地
        self.isDataUpdated = YES;
        //因为不是第一次加载数据,SonicClient会直接把缓存内容丢给SonicURLProtocol,didFinishCacheRead就是用来标记是否已经丢给了SonicURLProtocol
        //这个if一般都是不执行的
        if (!self.didFinishCacheRead) {
            return;
        }
        dispatchToMain(^{
            //获取服务器对于该客户端指派的行为
            //true:缓存到磁盘并展示返回内容
            //false:展示返回内容,无需缓存到磁盘
            //store:缓存到磁盘,如果已经加载缓存,则下次加载,否则展示返回内容
            //http:容灾字段,如果http表示终端六个小时之内不会采用sonic请求该URL
            NSString *policy = [self responseHeaderValueByIgnoreCaseKey:SonicHeaderKeyCacheOffline];
            //true false需要重新更新一下界面
            //因为模版有更新,当前webView显示的内容是本地缓存的
            if ([policy isEqualToString:SonicHeaderValueCacheOfflineStoreRefresh] || [policy isEqualToString:SonicHeaderValueCacheOfflineRefresh]) {
                if (self.delegate && [self.delegate respondsToSelector:@selector(session:requireWebViewReload:)]) {
                    NSURLRequest *sonicRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:self.url]];
                    [self.delegate session:self requireWebViewReload:sonicWebRequest(sonicRequest)];
                }
            }
        });
    }
}
//如果是动态数据变化
- (void)dealWithDataUpdate {
    //只需要把变化的部分写入
    SonicCacheItem *cacheItem = [[SonicCache shareCache] updateWithJsonData:self.responseData withResponseHeaders:self.response.allHeaderFields withUrl:self.url];
    if (cacheItem) {
        self.sonicStatusCode = SonicStatusCodeDataUpdate;
        self.sonicStatusFinalCode = SonicStatusCodeDataUpdate;
        self.localRefreshTime = cacheItem.lastRefreshTime;
        self.cacheFileData = cacheItem.htmlData;
        self.cacheResponseHeaders = cacheItem.cacheResponseHeaders;
        if (_diffData) {
            [_diffData release];
            _diffData = nil;
        }
        //把这次请求和之前本地保存的数据不同的部分保存起来
        _diffData = [cacheItem.diffData copy];
        //标志数据更新到本地完成
        self.isDataUpdated = YES;
    }
}
</code></pre> 
 <p>其中要注意的是模版更新需要让webView重新loadRequest,也就是WebViewController.m下面的代码会执行并且SonicURLProtocol会重新拦截一次</p> 
 <pre><code>- (void)session:(SonicSession *)session requireWebViewReload:(NSURLRequest *)request{
    [self.webView loadRequest:request];
}
</code></pre> 
 <p>数据更新需要把diffData通知给前端,也就是WebViewController.m下面的方法会被调用</p> 
 <pre><code>__weak typeof(webViewJavascriptBridge) weakWebViewJavascriptBridge = webViewJavascriptBridge;
//简单动态数据变化,传给前端
[[SonicClient sharedClient] sonicUpdateDiffDataByWebDelegate:self completion:^(NSDictionary *result) {
   __strong typeof(webViewJavascriptBridge) strongWebViewJavascriptBridge = weakWebViewJavascriptBridge;
   if (result) {
        NSData *json = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
        NSString *jsonStr = [[NSString alloc]initWithData:json encoding:NSUTF8StringEncoding];
       [strongWebViewJavascriptBridge callHandler:@"getDiffDataCallback" data:jsonStr];
     }
}];
</code></pre> 
 <p>那么数据变化的请求代码也走完了,是不是觉得还是很简单呢</p> 
 <h2>完全缓存</h2> 
 <p>完全缓存比起数据更新情况前面都是一样的只是在SonicSession的updateDidSuccess有不同</p> 
 <pre><code>- (void)updateDidSuccess {
    if (![self isCompletionWithOutError])
        return;
    switch (self.response.statusCode) {
            //服务器返回304表示完全缓存
        case 304: {
            //记录下来 就可以直接返回了,因为完全缓存客户端并不需要做什么
            self.sonicStatusCode = SonicStatusCodeAllCached;
            self.sonicStatusFinalCode = SonicStatusCodeAllCached;
        }
...
}
</code></pre> 
 <p>这里会走304代码块,只是记录一下状态,然后就没有其他的操作了</p> 
 <h2>后记</h2> 
 <p>写这篇文章前我和我公司服务器相关人员一起研究了3.5天弄懂了Sonic工作原理,然后花了2天看iOS端的SonicSDK的源码,最后写这篇文章用了2天;前前后后收获很多,最开始自己不知道文章从何写起,到现在我已经能按照自己思想重新组装SonicSDK了;文章我自认为你们看了还是有很多不懂的地方,虽然我已经自己阅读了两遍;这个技术非常有意思,对于应用内部打开网页很慢的问题这就是一个良药;最后感谢自己的群小伙伴们每天自认为忙的要死,作为群主的我自然想让他们多学一点知识,就有了写这篇文章的动力;最后希望你们多多在文章下提出不懂的地方我好随时修改,让大家都受益。</p> 
</article>
                            </div>
                        </div>
                    </div>
                    <!--PC和WAP自适应版-->
                    <div id="SOHUCS" sid="1276514413192429568"></div>
                    <script type="text/javascript" src="/views/front/js/chanyan.js"></script>
                    <!-- 文章页-底部 动态广告位 -->
                    <div class="youdao-fixed-ad" id="detail_ad_bottom"></div>
                </div>
                <div class="col-md-3">
                    <div class="row" id="ad">
                        <!-- 文章页-右侧1 动态广告位 -->
                        <div id="right-1" class="col-lg-12 col-md-12 col-sm-4 col-xs-4 ad">
                            <div class="youdao-fixed-ad" id="detail_ad_1"> </div>
                        </div>
                        <!-- 文章页-右侧2 动态广告位 -->
                        <div id="right-2" class="col-lg-12 col-md-12 col-sm-4 col-xs-4 ad">
                            <div class="youdao-fixed-ad" id="detail_ad_2"></div>
                        </div>
                        <!-- 文章页-右侧3 动态广告位 -->
                        <div id="right-3" class="col-lg-12 col-md-12 col-sm-4 col-xs-4 ad">
                            <div class="youdao-fixed-ad" id="detail_ad_3"></div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <div class="container">
        <h4 class="pt20 mb15 mt0 border-top">你可能感兴趣的:(VasSonic深度剖析之iOS端)</h4>
        <div id="paradigm-article-related">
            <div class="recommend-post mb30">
                <ul class="widget-links">
                    <li><a href="/article/1943993533169987584.htm"
                           title="C++11堆操作深度解析:std::is_heap与std::is_heap_until原理解析与实践" target="_blank">C++11堆操作深度解析:std::is_heap与std::is_heap_until原理解析与实践</a>
                        <span class="text-muted"></span>

                        <div>文章目录堆结构基础与函数接口堆的核心性质函数签名与核心接口std::is_heapstd::is_heap_until实现原理深度剖析std::is_heap的验证逻辑std::is_heap_until的定位策略算法优化细节代码实践与案例分析基础用法演示自定义比较器实现最小堆检查边缘情况处理性能分析与实际应用时间复杂度对比典型应用场景与手动实现的对比注意事项与最佳实践迭代器要求比较器设计C++标</div>
                    </li>
                    <li><a href="/article/1943983064719880192.htm"
                           title="分布式学习笔记_04_复制模型" target="_blank">分布式学习笔记_04_复制模型</a>
                        <span class="text-muted">NzuCRAS</span>
<a class="tag" taget="_blank" href="/search/%E5%88%86%E5%B8%83%E5%BC%8F/1.htm">分布式</a><a class="tag" taget="_blank" href="/search/%E5%AD%A6%E4%B9%A0/1.htm">学习</a><a class="tag" taget="_blank" href="/search/%E7%AC%94%E8%AE%B0/1.htm">笔记</a><a class="tag" taget="_blank" href="/search/%E6%9E%B6%E6%9E%84/1.htm">架构</a><a class="tag" taget="_blank" href="/search/%E5%90%8E%E7%AB%AF/1.htm">后端</a>
                        <div>常见复制模型使用复制的目的在分布式系统中,数据通常需要被分布在多台机器上,主要为了达到:拓展性:数据量因读写负载巨大,一台机器无法承载,数据分散在多台机器上仍然可以有效地进行负载均衡,达到灵活的横向拓展高容错&高可用:在分布式系统中单机故障是常态,在单机故障的情况下希望整体系统仍然能够正常工作,这时候就需要数据在多台机器上做冗余,在遇到单机故障时能够让其他机器接管统一的用户体验:如果系统客户端分布</div>
                    </li>
                    <li><a href="/article/1943983065500020736.htm"
                           title="Python之七彩花朵代码实现" target="_blank">Python之七彩花朵代码实现</a>
                        <span class="text-muted">PlutoZuo</span>
<a class="tag" taget="_blank" href="/search/Python/1.htm">Python</a><a class="tag" taget="_blank" href="/search/python/1.htm">python</a><a class="tag" taget="_blank" href="/search/%E5%BC%80%E5%8F%91%E8%AF%AD%E8%A8%80/1.htm">开发语言</a>
                        <div>Python之七彩花朵代码实现文章目录Python之七彩花朵代码实现下面是一个简单的使用Python的七彩花朵。这个示例只是一个简单的版本,没有很多高级功能,但它可以作为一个起点,你可以在此基础上添加更多功能。importturtleastuimportrandomasraimportmathtu.setup(1.0,1.0)t=tu.Pen()t.ht()colors=['red','skybl</div>
                    </li>
                    <li><a href="/article/1943976888804372480.htm"
                           title="Redis Sentinel(哨兵) 和 Redis Cluster(集群)" target="_blank">Redis Sentinel(哨兵) 和 Redis Cluster(集群)</a>
                        <span class="text-muted">G丶AEOM</span>
<a class="tag" taget="_blank" href="/search/%E5%85%AB%E8%82%A1/1.htm">八股</a><a class="tag" taget="_blank" href="/search/%E6%99%AE%E9%80%9A%E5%AD%A6%E4%B9%A0%E5%8C%BA/1.htm">普通学习区</a><a class="tag" taget="_blank" href="/search/Redis/1.htm">Redis</a><a class="tag" taget="_blank" href="/search/redis/1.htm">redis</a><a class="tag" taget="_blank" href="/search/%E6%95%B0%E6%8D%AE%E5%BA%93/1.htm">数据库</a><a class="tag" taget="_blank" href="/search/%E7%BC%93%E5%AD%98/1.htm">缓存</a>
                        <div>哨兵机制和集群有什么区别Redis集群主要有两种,一种是RedisSentinel哨兵集群,一种是RedisCluster。主从集群,包括一个Master和多个Slave节点,Master负责数据的读写,Slave负责数据的读取,Master上收到的数据变更会同步到Slave节点上实现数据同步,但不提供容错和恢复,在Master宕机时不会选出新的Master,导致后续客户端所有写请求直接失败。所以</div>
                    </li>
                    <li><a href="/article/1943974618851241984.htm"
                           title="Vue3+Vite+TS+Axios整合详细教程" target="_blank">Vue3+Vite+TS+Axios整合详细教程</a>
                        <span class="text-muted">老马聊技术</span>
<a class="tag" taget="_blank" href="/search/Vue/1.htm">Vue</a><a class="tag" taget="_blank" href="/search/Vite/1.htm">Vite</a><a class="tag" taget="_blank" href="/search/TS/1.htm">TS</a><a class="tag" taget="_blank" href="/search/vue.js/1.htm">vue.js</a>
                        <div>1.Vite简介Vite是新一代的前端构建工具,在尤雨溪开发Vue3.0的时候诞生。类似于Webpack+Webpack-dev-server。其主要利用浏览器ESM特性导入组织代码,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用。生产中利用Rollup作为打包工具,号称下一代的前端构建工具。vite是一种新型的前端构建工具,能够显著的提升前端开发者的体验。它主要有俩部分组成:一个</div>
                    </li>
                    <li><a href="/article/1943972726150590464.htm"
                           title="Shader面试题100道之(81-100)" target="_blank">Shader面试题100道之(81-100)</a>
                        <span class="text-muted">还是大剑师兰特</span>
<a class="tag" taget="_blank" href="/search/%23/1.htm">#</a><a class="tag" taget="_blank" href="/search/Shader/1.htm">Shader</a><a class="tag" taget="_blank" href="/search/%E7%BB%BC%E5%90%88%E6%95%99%E7%A8%8B100%2B/1.htm">综合教程100+</a><a class="tag" taget="_blank" href="/search/%E5%A4%A7%E5%89%91%E5%B8%88/1.htm">大剑师</a><a class="tag" taget="_blank" href="/search/shader%E9%9D%A2%E8%AF%95%E9%A2%98/1.htm">shader面试题</a><a class="tag" taget="_blank" href="/search/shader%E6%95%99%E7%A8%8B/1.htm">shader教程</a>
                        <div>Shader面试题(第81-100题)以下是第81到第100道Shader相关的面试题及答案:81.Unity中如何实现屏幕空间的热扭曲效果(HeatDistortion)?热扭曲效果可以通过GrabPass抓取当前屏幕图像,然后在片段着色器中使用噪声或动态UV偏移模拟空气扰动,再结合一个透明通道控制扭曲强度来实现。82.Shader中如何实现物体轮廓高亮(OutlineHighlight)?轮廓</div>
                    </li>
                    <li><a href="/article/1943972220988616704.htm"
                           title="Java特性之设计模式【责任链模式】" target="_blank">Java特性之设计模式【责任链模式】</a>
                        <span class="text-muted">Naijia_OvO</span>
<a class="tag" taget="_blank" href="/search/Java%E7%89%B9%E6%80%A7/1.htm">Java特性</a><a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/1.htm">设计模式</a><a class="tag" taget="_blank" href="/search/%E8%B4%A3%E4%BB%BB%E9%93%BE%E6%A8%A1%E5%BC%8F/1.htm">责任链模式</a>
                        <div>一、责任链模式概述顾名思义,责任链模式(ChainofResponsibilityPattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为型模式在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推主要解决:职责链上的处理者负责处理请求,客户只需要将</div>
                    </li>
                    <li><a href="/article/1943968187645227008.htm"
                           title="Kafka系列之:Dead Letter Queue死信队列DLQ" target="_blank">Kafka系列之:Dead Letter Queue死信队列DLQ</a>
                        <span class="text-muted">快乐骑行^_^</span>
<a class="tag" taget="_blank" href="/search/Kafka/1.htm">Kafka</a><a class="tag" taget="_blank" href="/search/Kafka%E7%B3%BB%E5%88%97/1.htm">Kafka系列</a><a class="tag" taget="_blank" href="/search/Dead/1.htm">Dead</a><a class="tag" taget="_blank" href="/search/Letter/1.htm">Letter</a><a class="tag" taget="_blank" href="/search/Queue/1.htm">Queue</a><a class="tag" taget="_blank" href="/search/%E6%AD%BB%E4%BF%A1%E9%98%9F%E5%88%97/1.htm">死信队列</a><a class="tag" taget="_blank" href="/search/DLQ/1.htm">DLQ</a>
                        <div>Kafka系列之:DeadLetterQueue死信队列DLQ一、死信队列二、参数errors.tolerance三、创建死信队列主题四、在启用安全性的情况下使用死信队列更多内容请阅读博主这篇博客:Kafka系列之:KafkaConnect深入探讨-错误处理和死信队列一、死信队列死信队列(DLQ)仅适用于接收器连接器。当一条记录以JSON格式到达接收器连接器时,但接收器连接器配置期望另一种格式,如</div>
                    </li>
                    <li><a href="/article/1943967933940166656.htm"
                           title="蓝牙MTU含义 ,协商修改的过程案例分析" target="_blank">蓝牙MTU含义 ,协商修改的过程案例分析</a>
                        <span class="text-muted">悟空胆好小</span>
<a class="tag" taget="_blank" href="/search/%E5%B5%8C%E5%85%A5%E5%BC%8F%E7%A1%AC%E4%BB%B6/1.htm">嵌入式硬件</a><a class="tag" taget="_blank" href="/search/%E7%BD%91%E7%BB%9C/1.htm">网络</a><a class="tag" taget="_blank" href="/search/%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD/1.htm">人工智能</a>
                        <div>蓝牙MTU含义,协商修改的过程案例分析文章目录**蓝牙MTU含义,协商修改的过程案例分析****一、MTU含义解析****二、MTU协商过程详解****步骤流程****三、修改MTU的实践案例分析****案例1:中心设备主动设置(主控端)****案例2:外设端响应优化(从设备)****案例3:调试工具强制修改****四、关键限制与注意事项**蓝牙MTU(MaximumTransmissionUni</div>
                    </li>
                    <li><a href="/article/1943966169434222592.htm"
                           title="Maya自定义右键菜单样例教程" target="_blank">Maya自定义右键菜单样例教程</a>
                        <span class="text-muted">holy-pills</span>

                        <div>本文还有配套的精品资源,点击获取简介:本文详细指导如何在Maya中通过脚本节点自定义右键菜单,增强工作效率和个性化工作环境。自定义右键菜单允许用户根据个人习惯调整菜单项,使之更加便捷。文章介绍了创建脚本节点、编写菜单脚本、关联菜单到视图以及保存和加载自定义菜单的具体步骤。同时提供了实际操作样例,帮助用户更好地理解和应用这一技巧。1.Maya自定义右键菜单的重要性Maya,作为三维动画制作的行业标准</div>
                    </li>
                    <li><a href="/article/1943962514060537856.htm"
                           title="Spring Cloud Gateway 的执行链路详解" target="_blank">Spring Cloud Gateway 的执行链路详解</a>
                        <span class="text-muted">愤怒的代码</span>
<a class="tag" taget="_blank" href="/search/SpringCloud/1.htm">SpringCloud</a><a class="tag" taget="_blank" href="/search/spring/1.htm">spring</a><a class="tag" taget="_blank" href="/search/cloud/1.htm">cloud</a>
                        <div>SpringCloudGateway的执行链路详解核心目标明确SpringCloudGateway的请求处理全过程(从接收到请求→到转发→到返回响应),方便你在合适的生命周期节点插入你的逻辑。核心执行链路图(执行顺序)┌──────────────┐│客户端请求│└────┬─────────┘↓┌────┴─────────────┐│NettyHttpServer│←→ReactorNetty</div>
                    </li>
                    <li><a href="/article/1943962387782627328.htm"
                           title="Java 调用 HTTP 接口的 7 种方式:全网最全指南" target="_blank">Java 调用 HTTP 接口的 7 种方式:全网最全指南</a>
                        <span class="text-muted"></span>

                        <div>Java调用HTTP接口的7种方式:全网最全指南在开发过程中,调用HTTP接口是最常见的需求之一。本文将详细介绍Java中7种主流的调用HTTP接口的方式,包括每种工具的优缺点和完整代码实现。1.使用RestTemplateRestTemplate是Spring提供的同步HTTP客户端,适用于传统项目。尽管从Spring5开始被标记为过时,它仍然是许多开发者的首选。示例代码importorg.sp</div>
                    </li>
                    <li><a href="/article/1943962261408247808.htm"
                           title="rocketmq的重试队列和死信队列" target="_blank">rocketmq的重试队列和死信队列</a>
                        <span class="text-muted">还不够</span>
<a class="tag" taget="_blank" href="/search/MQ/1.htm">MQ</a>
                        <div>原文:https://www.jianshu.com/p/1281f7fee69c消费端,一直不回传消费的结果。rocketmq认为消息没收到,consumer下一次拉取,broker依然会发送该消息。所以,任何异常都要捕获返回ConsumeConcurrentlyStatus.RECONSUME_LATERrocketmq会放到重试队列。这个重试TOPIC的名字是%RETRY%+consumer</div>
                    </li>
                    <li><a href="/article/1943950289824444416.htm"
                           title="实时预览功能问题" target="_blank">实时预览功能问题</a>
                        <span class="text-muted">GISer_Jinger</span>
<a class="tag" taget="_blank" href="/search/%E9%A1%B9%E7%9B%AE/1.htm">项目</a><a class="tag" taget="_blank" href="/search/javascript/1.htm">javascript</a><a class="tag" taget="_blank" href="/search/%E5%BC%80%E5%8F%91%E8%AF%AD%E8%A8%80/1.htm">开发语言</a><a class="tag" taget="_blank" href="/search/ecmascript/1.htm">ecmascript</a>
                        <div>你遇到的问题是:“B端修改配置后无法实时出现在previewiframe中,而必须点击刷新才能生效”。主要原因与以下几方面有关:❗为什么需要手动刷新:iFrame与主页面之间缺少实时通信机制:原本仅靠刷新重新加载iframe,而没有通过postMessage等方式同步状态;Valtio的proxy状态不能跨文件热刷新持久保存:当你修改包含proxy定义的文件,热重载会导致object被替换,监听丢</div>
                    </li>
                    <li><a href="/article/1943950037142794240.htm"
                           title="Windows平台下Android Studio搭建Flutter开发环境的正确姿势(202506)" target="_blank">Windows平台下Android Studio搭建Flutter开发环境的正确姿势(202506)</a>
                        <span class="text-muted"></span>

                        <div>Flutter作为Google推出的跨平台移动应用开发框架,近年来获得了广泛关注。它允许开发者使用单一代码库构建iOS和Android应用,大大提高了开发效率。本文将带你一步步在Windows系统上搭建完整的Flutter开发环境。第一步:下载并安装FlutterSDK首先,我们需要获取FlutterSDK:访问Flutter官方中文文档的安装页面:https://docs.flutter.cn/</div>
                    </li>
                    <li><a href="/article/1943949027624153088.htm"
                           title="【AI大模型学习路线】第三阶段之RAG与LangChain——第十六章(Advanced RAG[1])基于历史对话重新生成Query?" target="_blank">【AI大模型学习路线】第三阶段之RAG与LangChain——第十六章(Advanced RAG[1])基于历史对话重新生成Query?</a>
                        <span class="text-muted">985小水博一枚呀</span>
<a class="tag" taget="_blank" href="/search/AI%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%AD%A6%E4%B9%A0%E8%B7%AF%E7%BA%BF/1.htm">AI大模型学习路线</a><a class="tag" taget="_blank" href="/search/%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD/1.htm">人工智能</a><a class="tag" taget="_blank" href="/search/%E5%AD%A6%E4%B9%A0/1.htm">学习</a><a class="tag" taget="_blank" href="/search/langchain/1.htm">langchain</a><a class="tag" taget="_blank" href="/search/RAG/1.htm">RAG</a>
                        <div>【AI大模型学习路线】第三阶段之RAG与LangChain——第十六章(AdvancedRAG[1])基于历史对话重新生成Query?【AI大模型学习路线】第三阶段之RAG与LangChain——第十六章(AdvancedRAG[1])基于历史对话重新生成Query?文章目录【AI大模型学习路线】第三阶段之RAG与LangChain——第十六章(AdvancedRAG[1])基于历史对话重新生成Q</div>
                    </li>
                    <li><a href="/article/1943949028291047424.htm"
                           title="【AI大模型学习路线】第三阶段之RAG与LangChain——第十六章(Advanced RAG[1])其他Query优化相关策略?" target="_blank">【AI大模型学习路线】第三阶段之RAG与LangChain——第十六章(Advanced RAG[1])其他Query优化相关策略?</a>
                        <span class="text-muted">985小水博一枚呀</span>
<a class="tag" taget="_blank" href="/search/AI%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%AD%A6%E4%B9%A0%E8%B7%AF%E7%BA%BF/1.htm">AI大模型学习路线</a><a class="tag" taget="_blank" href="/search/%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD/1.htm">人工智能</a><a class="tag" taget="_blank" href="/search/%E5%AD%A6%E4%B9%A0/1.htm">学习</a><a class="tag" taget="_blank" href="/search/langchain/1.htm">langchain</a>
                        <div>【AI大模型学习路线】第三阶段之RAG与LangChain——第十六章(AdvancedRAG[1])其他Query优化相关策略?【AI大模型学习路线】第三阶段之RAG与LangChain——第十六章(AdvancedRAG[1])其他Query优化相关策略?文章目录【AI大模型学习路线】第三阶段之RAG与LangChain——第十六章(AdvancedRAG[1])其他Query优化相关策略?一</div>
                    </li>
                    <li><a href="/article/1943949029331234816.htm"
                           title="C# 设计模式(结构型模式):组合模式" target="_blank">C# 设计模式(结构型模式):组合模式</a>
                        <span class="text-muted">硅谷调试员</span>
<a class="tag" taget="_blank" href="/search/%E7%8E%A9%E8%BD%ACC%23%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/1.htm">玩转C#设计模式</a><a class="tag" taget="_blank" href="/search/c%23/1.htm">c#</a><a class="tag" taget="_blank" href="/search/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/1.htm">设计模式</a><a class="tag" taget="_blank" href="/search/%E7%BB%84%E5%90%88%E6%A8%A1%E5%BC%8F/1.htm">组合模式</a>
                        <div>C#设计模式(结构型模式):组合模式在软件设计中,有时我们需要处理的是一组对象,而这些对象既可以是单独的元素,也可以是由多个子元素组成的复合体。这时,组合模式(CompositePattern)便能提供帮助。它允许客户端将单个对象和对象集合统一对待,从而简化了树形结构的管理。1.组合模式的定义组合模式是一个结构型设计模式,主要用于将多个对象组合成树形结构,以表示“部分-整体”的层次关系。通过组合模</div>
                    </li>
                    <li><a href="/article/1943932648590209024.htm"
                           title="AI 图像编辑提示词参考之:背景替换" target="_blank">AI 图像编辑提示词参考之:背景替换</a>
                        <span class="text-muted"></span>

                        <div>在AI图像编辑中(以FluxKontext为例),“替换背景”(BackgroundReplacement)是提升图像表现力的关键手段之一。但背景更换不仅仅是简单的视觉置换,更重要的是:确保人物主体外观不变,并与新背景在色温、色调、光影等方面自然融合。只有这样,最终图像才会呈现出“原本拍摄于该背景环境”的真实感。建议使用以下结构组织提示词:Replacethebackgroundwith[新背景]</div>
                    </li>
                    <li><a href="/article/1943932268338802688.htm"
                           title="redis集群之Sentinel哨兵高可用" target="_blank">redis集群之Sentinel哨兵高可用</a>
                        <span class="text-muted">会飞的爱迪生</span>
<a class="tag" taget="_blank" href="/search/redis/1.htm">redis</a><a class="tag" taget="_blank" href="/search/redis/1.htm">redis</a><a class="tag" taget="_blank" href="/search/sentinel/1.htm">sentinel</a><a class="tag" taget="_blank" href="/search/bootstrap/1.htm">bootstrap</a>
                        <div>Sentinel是官网推荐的高可用(HA)解决方案,可以实现redis的高可用,即主挂了从代替主工作,在一台单独的服务器上运行多个sentinel,去监控其他服务器上的redismaster-slave状态(可以监控多个master-slave),当发现master宕机后sentinel会在slave中选举并启动新的master。至少需要3台redis才能建立起基于哨兵的reids集群。一、通过s</div>
                    </li>
                    <li><a href="/article/1943930755528847360.htm"
                           title="在 Windows 上安装 Docker Desktop" target="_blank">在 Windows 上安装 Docker Desktop</a>
                        <span class="text-muted">不老刘</span>
<a class="tag" taget="_blank" href="/search/%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD/1.htm">人工智能</a><a class="tag" taget="_blank" href="/search/windows/1.htm">windows</a><a class="tag" taget="_blank" href="/search/docker/1.htm">docker</a><a class="tag" taget="_blank" href="/search/%E5%AE%B9%E5%99%A8/1.htm">容器</a>
                        <div>还是简单说一下,如何在Windows上安装DockerDesktop,具体步骤如下:系统要求Windows10/1164-bit(专业版、企业版或教育版,版本21H2或更高)启用WSL2(WindowsSubsystemforLinux2)或Hyper-V至少4GB内存BIOS中启用虚拟化(VT-x/AMD-V)安装步骤1.下载DockerDesktop访问Docker官网下载页面。下载Docke</div>
                    </li>
                    <li><a href="/article/1943930501668597760.htm"
                           title=".NET中的安全性之数字签名、数字证书、强签名程序集、反编译" target="_blank">.NET中的安全性之数字签名、数字证书、强签名程序集、反编译</a>
                        <span class="text-muted">hezudao25</span>
<a class="tag" taget="_blank" href="/search/NET/1.htm">NET</a><a class="tag" taget="_blank" href="/search/.net/1.htm">.net</a><a class="tag" taget="_blank" href="/search/assembly/1.htm">assembly</a><a class="tag" taget="_blank" href="/search/%E5%8A%A0%E5%AF%86/1.htm">加密</a><a class="tag" taget="_blank" href="/search/%E7%AE%97%E6%B3%95/1.htm">算法</a><a class="tag" taget="_blank" href="/search/reference/1.htm">reference</a><a class="tag" taget="_blank" href="/search/header/1.htm">header</a>
                        <div>本文将探讨数字签名、数字证书、强签名程序集、反编译等以及它们在.NET中的运用(一些概念并不局限于.NET在其它技术、平台中也存在)。1.数字签名数字签名又称为公钥数字签名,或者电子签章等,它借助公钥加密技术实现。数字签名技术主要涉及公钥、私钥、非对称加密算法。1.1公钥与私钥公钥是公开的钥匙,私钥则是与公钥匹配的严格保护的私有密钥;私钥加密的信息只有公钥可以解开,反之亦然。在VisualStud</div>
                    </li>
                    <li><a href="/article/1943930248785620992.htm"
                           title="QML与C++相互调用函数并获得返回值" target="_blank">QML与C++相互调用函数并获得返回值</a>
                        <span class="text-muted">cpp_learners</span>
<a class="tag" taget="_blank" href="/search/QML/1.htm">QML</a><a class="tag" taget="_blank" href="/search/c%2B%2B/1.htm">c++</a><a class="tag" taget="_blank" href="/search/QML/1.htm">QML</a><a class="tag" taget="_blank" href="/search/qt/1.htm">qt</a>
                        <div>这篇博客主要讲解在qml端如何直接调用c++的函数并获得返回值,在c++端如何直接调用qml的函数并获得返回值;主要以map或者jsonobject、list或者jsonarray为主!其他单个类型,常见的类型,例如QString、int等,就不演示了;一通百通。目录1准备工作1.1C++端1.2QML端2qml端直接调用c++端函数3c++端直接调用qml端函数3.1调用qml的qmlFuncO</div>
                    </li>
                    <li><a href="/article/1943929867959595008.htm"
                           title="AI Agent开发学习系列 - langchain之Chains的使用(7):用四种处理文档的预制链轻松实现文档对话" target="_blank">AI Agent开发学习系列 - langchain之Chains的使用(7):用四种处理文档的预制链轻松实现文档对话</a>
                        <span class="text-muted">alex100</span>
<a class="tag" taget="_blank" href="/search/AI/1.htm">AI</a><a class="tag" taget="_blank" href="/search/Agent/1.htm">Agent</a><a class="tag" taget="_blank" href="/search/%E5%AD%A6%E4%B9%A0/1.htm">学习</a><a class="tag" taget="_blank" href="/search/%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD/1.htm">人工智能</a><a class="tag" taget="_blank" href="/search/langchain/1.htm">langchain</a><a class="tag" taget="_blank" href="/search/prompt/1.htm">prompt</a><a class="tag" taget="_blank" href="/search/%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B/1.htm">语言模型</a><a class="tag" taget="_blank" href="/search/python/1.htm">python</a>
                        <div>在LangChain中,四种文档处理预制链(stuff、refine、mapreduce、mapre-rank)是实现文档问答、摘要等任务的常用高阶工具。它们的核心作用是:将长文档切分为块,分步处理,再整合结果,极大提升大模型处理长文档的能力。stuff直接拼接所有文档内容到prompt,一次性交给大模型处理。适合文档较短、token不超限的场景。refine递进式摘要。先对第一块文档生成初步答案</div>
                    </li>
                    <li><a href="/article/1943923817982259200.htm"
                           title="Java Web 之 Session 详解" target="_blank">Java Web 之 Session 详解</a>
                        <span class="text-muted">艾伦~耶格尔</span>
<a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/%E5%BC%80%E5%8F%91%E8%AF%AD%E8%A8%80/1.htm">开发语言</a><a class="tag" taget="_blank" href="/search/%E5%90%8E%E7%AB%AF/1.htm">后端</a><a class="tag" taget="_blank" href="/search/%E5%89%8D%E7%AB%AF/1.htm">前端</a><a class="tag" taget="_blank" href="/search/session/1.htm">session</a>
                        <div>在JavaWeb开发中,Session就像网站的专属记忆管家,为每个用户保管着重要的信息和状态,确保用户在网站的旅程顺畅无阻。场景一:想象你去一家大型超市购物,推着购物车挑选商品。这个购物车就如同Session,它记录了你的购物信息,方便你在结账时一次性结算。场景二:你在玩一个在线游戏,登录账号后,你的游戏进度、等级、装备等信息都会被保存在Session中,即使你中途关闭游戏,下次登录时依然可以继</div>
                    </li>
                    <li><a href="/article/1943921676257390592.htm"
                           title="iOS 多个线程对数组操作(遍历,插入,删除),实现一个线程安全的NSMutabeArray" target="_blank">iOS 多个线程对数组操作(遍历,插入,删除),实现一个线程安全的NSMutabeArray</a>
                        <span class="text-muted"></span>

                        <div>//联系人:石虎QQ:1224614774昵称:嗡嘛呢叭咪哄一、概念1.含义:@synchronized(self){}//这个其实就是一个加锁。如果self其他线程访问,则会阻塞。这样做一般是用来对单2.重写构造方法@interfaceSHSafetyArray:NSObject{@privateNSMutableArray*_mutableArray;//声明数组}//遍历加锁-(void)m</div>
                    </li>
                    <li><a href="/article/1943920414254231552.htm"
                           title="iOS 获取Wifi信息" target="_blank">iOS 获取Wifi信息</a>
                        <span class="text-muted"></span>

                        <div>背景智能硬件入网的时候,硬件端通常需要通过WiFi入网,这种情况,可能需要App获取WiFi信息,来做WiFi匹配,同时也可以减少用户填写的信息。智能硬件WiFi入网方式利用HomeKit流程入网,然后利用Boujour绑定设备App连接硬件WiFi,将有网WiFi的信息通过http或者蓝牙方式发送给硬件(需要用户选择wifi,且可能有网WiFi是硬件不支持的,比如部分硬件不支持5GWiFi)部分</div>
                    </li>
                    <li><a href="/article/1943919531676200960.htm"
                           title="iOS线程安全数组" target="_blank">iOS线程安全数组</a>
                        <span class="text-muted"></span>

                        <div>iOS-SDK只提供了非线程安全的数组。如果要多线程并发的使用一个数组对象就必须要加锁,平凡的加锁使得代码的调用非常的麻烦。我们需要多线程的读写锁在类的内部实现,所以需要对NSMutableArray进行封装,封装后的对象负责接受所有事件并将其转发给真正的NSMutableArrayiOS-SDK只提供了非线程安全的数组。如果要多线程并发的使用一个数组对象就必须要加锁,平凡的加锁使得代码的调用非常</div>
                    </li>
                    <li><a href="/article/1943919153068961792.htm"
                           title="Ajax之核心语法详解" target="_blank">Ajax之核心语法详解</a>
                        <span class="text-muted">AA-代码批发V哥</span>
<a class="tag" taget="_blank" href="/search/Ajax%2FAxios/1.htm">Ajax/Axios</a><a class="tag" taget="_blank" href="/search/ajax/1.htm">ajax</a>
                        <div>Ajax之核心语法详解一、Ajax的核心原理与优势1.1什么是Ajax?1.2Ajax的优势二、XMLHttpRequest:Ajax的核心对象2.1XHR的基本使用流程2.2核心属性与事件解析2.2.1`readyState`:请求状态2.2.2`status`:HTTP状态码2.2.3响应数据属性2.2.4常用事件三、HTTP请求方法与数据传递3.1GET请求:获取数据3.2POST请求:提交</div>
                    </li>
                    <li><a href="/article/1943919026744913920.htm"
                           title="JavaScript之DOM操作与事件处理详解" target="_blank">JavaScript之DOM操作与事件处理详解</a>
                        <span class="text-muted">AA-代码批发V哥</span>
<a class="tag" taget="_blank" href="/search/JavaScript/1.htm">JavaScript</a><a class="tag" taget="_blank" href="/search/javascript/1.htm">javascript</a>
                        <div>JavaScript之DOM操作与事件处理详解一、DOM基础:理解文档对象模型二、DOM元素的获取与访问2.1基础获取方法2.2集合的区别与注意事项三、DOM元素的创建与修改3.1创建与插入元素3.2修改元素属性与样式3.2.1属性操作3.2.2样式操作3.3元素内容的修改四、DOM元素的删除与替换4.1删除元素4.2替换元素五、事件处理:实现页面交互5.1事件绑定的三种方式5.1.1HTML属性</div>
                    </li>
                                <li><a href="/article/70.htm"
                                       title="PHP如何实现二维数组排序?" target="_blank">PHP如何实现二维数组排序?</a>
                                    <span class="text-muted">IT独行者</span>
<a class="tag" taget="_blank" href="/search/%E4%BA%8C%E7%BB%B4%E6%95%B0%E7%BB%84/1.htm">二维数组</a><a class="tag" taget="_blank" href="/search/PHP/1.htm">PHP</a><a class="tag" taget="_blank" href="/search/%E6%8E%92%E5%BA%8F%E3%80%80/1.htm">排序 </a>
                                    <div>二维数组在PHP开发中经常遇到,但是他的排序就不如一维数组那样用内置函数来的方便了,(一维数组排序可以参考本站另一篇文章【PHP中数组排序函数详解汇总】)。二维数组的排序需要我们自己写函数处理了,这里UncleToo给大家分享一个PHP二维数组排序的函数: 
代码: 
functionarray_sort($arr,$keys,$type='asc'){
$keysvalue= $new_arr</div>
                                </li>
                                <li><a href="/article/197.htm"
                                       title="【Hadoop十七】HDFS HA配置" target="_blank">【Hadoop十七】HDFS HA配置</a>
                                    <span class="text-muted">bit1129</span>
<a class="tag" taget="_blank" href="/search/hadoop/1.htm">hadoop</a>
                                    <div>基于Zookeeper的HDFS HA配置主要涉及两个文件,core-site和hdfs-site.xml。 
  
测试环境有三台 
hadoop.master 
hadoop.slave1 
hadoop.slave2 
  
hadoop.master包含的组件NameNode, JournalNode, Zookeeper,DFSZKFailoverController</div>
                                </li>
                                <li><a href="/article/324.htm"
                                       title="由wsdl生成的java vo类不适合做普通java vo" target="_blank">由wsdl生成的java vo类不适合做普通java vo</a>
                                    <span class="text-muted">darrenzhu</span>
<a class="tag" taget="_blank" href="/search/VO/1.htm">VO</a><a class="tag" taget="_blank" href="/search/wsdl/1.htm">wsdl</a><a class="tag" taget="_blank" href="/search/webservice/1.htm">webservice</a><a class="tag" taget="_blank" href="/search/rpc/1.htm">rpc</a>
                                    <div>开发java webservice项目时,如果我们通过SOAP协议来输入输出,我们会利用工具从wsdl文件生成webservice的client端类,但是这里面生成的java data model类却不适合做为项目中的普通java vo类来使用,当然有一中情况例外,如果这个自动生成的类里面的properties都是基本数据类型,就没问题,但是如果有集合类,就不行。原因如下: 
1)使用了集合如Li</div>
                                </li>
                                <li><a href="/article/451.htm"
                                       title="JAVA海量数据处理之二(BitMap)" target="_blank">JAVA海量数据处理之二(BitMap)</a>
                                    <span class="text-muted">周凡杨</span>
<a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/%E7%AE%97%E6%B3%95/1.htm">算法</a><a class="tag" taget="_blank" href="/search/bitmap/1.htm">bitmap</a><a class="tag" taget="_blank" href="/search/bitset/1.htm">bitset</a><a class="tag" taget="_blank" href="/search/%E6%95%B0%E6%8D%AE/1.htm">数据</a>
                                    <div>         路漫漫其修远兮,吾将上下而求索。想要更快,就要深入挖掘 JAVA 基础的数据结构,从来分析出所编写的 JAVA 代码为什么把内存耗尽,思考有什么办法可以节省内存呢?   啊哈!算法。这里采用了 BitMap 思想。  
   
首先来看一个实验:  
指定 VM 参数大小: -Xms256m -Xmx540m </div>
                                </li>
                                <li><a href="/article/578.htm"
                                       title="java类型与数据库类型" target="_blank">java类型与数据库类型</a>
                                    <span class="text-muted">g21121</span>
<a class="tag" taget="_blank" href="/search/java/1.htm">java</a>
                                    <div>很多时候我们用hibernate的时候往往并不是十分关心数据库类型和java类型的对应关心,因为大多数hbm文件是自动生成的,但有些时候诸如:数据库设计、没有生成工具、使用原始JDBC、使用mybatis(ibatIS)等等情况,就会手动的去对应数据库与java的数据类型关心,当然比较简单的数据类型即使配置错了也会很快发现问题,但有些数据类型却并不是十分常见,这就给程序员带来了很多麻烦。 
&nb</div>
                                </li>
                                <li><a href="/article/705.htm"
                                       title="Linux命令" target="_blank">Linux命令</a>
                                    <span class="text-muted">510888780</span>
<a class="tag" taget="_blank" href="/search/linux%E5%91%BD%E4%BB%A4/1.htm">linux命令</a>
                                    <div>系统信息 
arch 显示机器的处理器架构(1) 
uname -m 显示机器的处理器架构(2) 
uname -r 显示正在使用的内核版本 
dmidecode -q 显示硬件系统部件 - (SMBIOS / DMI) 
hdparm -i /dev/hda 罗列一个磁盘的架构特性 
hdparm -tT /dev/sda 在磁盘上执行测试性读取操作 
cat /proc/cpuinfo 显示C</div>
                                </li>
                                <li><a href="/article/832.htm"
                                       title="java常用JVM参数" target="_blank">java常用JVM参数</a>
                                    <span class="text-muted">墙头上一根草</span>
<a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/jvm%E5%8F%82%E6%95%B0/1.htm">jvm参数</a>
                                    <div>-Xms:初始堆大小,默认为物理内存的1/64(<1GB);默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制 
-Xmx:最大堆大小,默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制 
-Xmn:新生代的内存空间大小,注意:此处的大小是(eden+ 2</div>
                                </li>
                                <li><a href="/article/959.htm"
                                       title="我的spring学习笔记9-Spring使用工厂方法实例化Bean的注意点" target="_blank">我的spring学习笔记9-Spring使用工厂方法实例化Bean的注意点</a>
                                    <span class="text-muted">aijuans</span>
<a class="tag" taget="_blank" href="/search/Spring+3/1.htm">Spring 3</a>
                                    <div>方法一:

    <bean id="musicBox" class="onlyfun.caterpillar.factory.MusicBoxFactory" 
    factory-method="createMusicBoxStatic"></bean>
    

    方法二:
   </div>
                                </li>
                                <li><a href="/article/1086.htm"
                                       title="mysql查询性能优化之二" target="_blank">mysql查询性能优化之二</a>
                                    <span class="text-muted">annan211</span>
<a class="tag" taget="_blank" href="/search/UNION/1.htm">UNION</a><a class="tag" taget="_blank" href="/search/mysql/1.htm">mysql</a><a class="tag" taget="_blank" href="/search/%E6%9F%A5%E8%AF%A2%E4%BC%98%E5%8C%96/1.htm">查询优化</a><a class="tag" taget="_blank" href="/search/%E7%B4%A2%E5%BC%95%E4%BC%98%E5%8C%96/1.htm">索引优化</a>
                                    <div>

1 union的限制
  有时mysql无法将限制条件从外层下推到内层,这使得原本能够限制部分返回结果的条件无法应用到内层
  查询的优化上。
  如果希望union的各个子句能够根据limit只取部分结果集,或者希望能够先排好序在
  合并结果集的话,就需要在union的各个子句中分别使用这些子句。
  
  例如 想将两个子查询结果联合起来,然后再取前20条记录,那么mys</div>
                                </li>
                                <li><a href="/article/1213.htm"
                                       title="数据的备份与恢复" target="_blank">数据的备份与恢复</a>
                                    <span class="text-muted">百合不是茶</span>
<a class="tag" taget="_blank" href="/search/oracle/1.htm">oracle</a><a class="tag" taget="_blank" href="/search/sql/1.htm">sql</a><a class="tag" taget="_blank" href="/search/%E6%95%B0%E6%8D%AE%E6%81%A2%E5%A4%8D/1.htm">数据恢复</a><a class="tag" taget="_blank" href="/search/%E6%95%B0%E6%8D%AE%E5%A4%87%E4%BB%BD/1.htm">数据备份</a>
                                    <div> 数据的备份与恢复的方式有: 表,方案 ,数据库; 
  
  
数据的备份: 
导出到的常见命令; 
参数                        说明
USERID         确定执行导出实用程序的用户名和口令
BUFFER        确定导出数据时所使用的缓冲区大小,其大小用字节表示
FILE               指定导出的二进制文</div>
                                </li>
                                <li><a href="/article/1340.htm"
                                       title="线程组" target="_blank">线程组</a>
                                    <span class="text-muted">bijian1013</span>
<a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/%E5%A4%9A%E7%BA%BF%E7%A8%8B/1.htm">多线程</a><a class="tag" taget="_blank" href="/search/thread/1.htm">thread</a><a class="tag" taget="_blank" href="/search/java%E5%A4%9A%E7%BA%BF%E7%A8%8B/1.htm">java多线程</a><a class="tag" taget="_blank" href="/search/%E7%BA%BF%E7%A8%8B%E7%BB%84/1.htm">线程组</a>
                                    <div>有些程序包含了相当数量的线程。这时,如果按照线程的功能将他们分成不同的类别将很有用。 
       线程组可以用来同时对一组线程进行操作。 
       创建线程组:ThreadGroup g = new ThreadGroup(groupName); 
 &nbs</div>
                                </li>
                                <li><a href="/article/1467.htm"
                                       title="top命令找到占用CPU最高的java线程" target="_blank">top命令找到占用CPU最高的java线程</a>
                                    <span class="text-muted">bijian1013</span>
<a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/linux/1.htm">linux</a><a class="tag" taget="_blank" href="/search/top/1.htm">top</a>
                                    <div>上次分析系统中占用CPU高的问题,得到一些使用Java自身调试工具的经验,与大家分享。 (1)使用top命令找出占用cpu最高的JAVA进程PID:28174 (2)如下命令找出占用cpu最高的线程 
top -Hp 28174 -d 1 -n 1
32694 root      20   0 3249m 2.0g  11m S    2  6.4   3:31.12 java         </div>
                                </li>
                                <li><a href="/article/1594.htm"
                                       title="【持久化框架MyBatis3四】MyBatis3一对一关联查询" target="_blank">【持久化框架MyBatis3四】MyBatis3一对一关联查询</a>
                                    <span class="text-muted">bit1129</span>
<a class="tag" taget="_blank" href="/search/Mybatis3/1.htm">Mybatis3</a>
                                    <div>  
当两个实体具有1对1的对应关系时,可以使用One-To-One的进行映射关联查询 
  One-To-One示例数据 
以学生表Student和地址信息表为例,每个学生都有都有1个唯一的地址(现实中,这种对应关系是不合适的,因为人和地址是多对一的关系),这里只是演示目的 
  
学生表 
  
CREATE TABLE STUDENTS 
(
  </div>
                                </li>
                                <li><a href="/article/1721.htm"
                                       title="C/C++图片或文件的读写" target="_blank">C/C++图片或文件的读写</a>
                                    <span class="text-muted">bitcarter</span>
<a class="tag" taget="_blank" href="/search/%E5%86%99%E5%9B%BE%E7%89%87/1.htm">写图片</a>
                                    <div>先看代码: 
 
 

/*strTmpResult是文件或图片字符串
 * filePath文件需要写入的地址或路径
 */
int writeFile(std::string &strTmpResult,std::string &filePath)
{
    int i,len = strTmpResult.length();
    	unsigned cha</div>
                                </li>
                                <li><a href="/article/1848.htm"
                                       title="nginx自定义指定加载配置" target="_blank">nginx自定义指定加载配置</a>
                                    <span class="text-muted">ronin47</span>

                                    <div>进入 /usr/local/nginx/conf/include 目录,创建 nginx.node.conf 文件,在里面输入如下代码:  
upstream nodejs {
    server 127.0.0.1:3000;
    #server 127.0.0.1:3001;
    keepalive 64;
}

server {
    liste</div>
                                </li>
                                <li><a href="/article/1975.htm"
                                       title="java-71-数值的整数次方.实现函数double Power(double base, int exponent),求base的exponent次方" target="_blank">java-71-数值的整数次方.实现函数double Power(double base, int exponent),求base的exponent次方</a>
                                    <span class="text-muted">bylijinnan</span>
<a class="tag" taget="_blank" href="/search/double/1.htm">double</a>
                                    <div>

public class Power {

	/**
	 *Q71-数值的整数次方
	 *实现函数double Power(double base, int exponent),求base的exponent次方。不需要考虑溢出。
	 */
	private static boolean InvalidInput=false;
	public static void main(</div>
                                </li>
                                <li><a href="/article/2102.htm"
                                       title="Android四大组件的理解" target="_blank">Android四大组件的理解</a>
                                    <span class="text-muted">Cb123456</span>
<a class="tag" taget="_blank" href="/search/android/1.htm">android</a><a class="tag" taget="_blank" href="/search/%E5%9B%9B%E5%A4%A7%E7%BB%84%E4%BB%B6%E7%9A%84%E7%90%86%E8%A7%A3/1.htm">四大组件的理解</a>
                                    <div> 分享一下,今天在Android开发文档-开发者指南中看到的:          
         
          App components are the essential building blocks of an Android </div>
                                </li>
                                <li><a href="/article/2229.htm"
                                       title="[宇宙与计算]涡旋场计算与拓扑分析" target="_blank">[宇宙与计算]涡旋场计算与拓扑分析</a>
                                    <span class="text-muted">comsci</span>
<a class="tag" taget="_blank" href="/search/%E8%AE%A1%E7%AE%97/1.htm">计算</a>
                                    <div> 
 
     怎么阐述我这个理论呢? 。。。。。。。。。 
 
 
      首先: 宇宙是一个非线性的拓扑结构与涡旋轨道时空的统一体。。。。 
 
      我们要在宇宙中寻找到一个适合人类居住的行星,时间非常重要,早一个刻度和晚一个刻度,这颗行星的</div>
                                </li>
                                <li><a href="/article/2356.htm"
                                       title="同一个Tomcat不同Web应用之间共享会话Session" target="_blank">同一个Tomcat不同Web应用之间共享会话Session</a>
                                    <span class="text-muted">cwqcwqmax9</span>
<a class="tag" taget="_blank" href="/search/session/1.htm">session</a>
                                    <div>实现两个WEB之间通过session 共享数据 
 
查看tomcat 关于 HTTP Connector 中有个emptySessionPath 其解释如下: 
 
If set to true, all paths for session cookies will be set to /. This can be useful for portlet specification impleme</div>
                                </li>
                                <li><a href="/article/2483.htm"
                                       title="springmvc Spring3 MVC,ajax,乱码" target="_blank">springmvc Spring3 MVC,ajax,乱码</a>
                                    <span class="text-muted">dashuaifu</span>
<a class="tag" taget="_blank" href="/search/spring/1.htm">spring</a><a class="tag" taget="_blank" href="/search/jquery/1.htm">jquery</a><a class="tag" taget="_blank" href="/search/mvc/1.htm">mvc</a><a class="tag" taget="_blank" href="/search/Ajax/1.htm">Ajax</a>
                                    <div>  
 springmvc Spring3 MVC @ResponseBody返回,jquery ajax调用中文乱码问题解决       
Spring3.0 MVC @ResponseBody 的作用是把返回值直接写到HTTP response body里。具体实现AnnotationMethodHandlerAdapter类handleResponseBody方法,具体实</div>
                                </li>
                                <li><a href="/article/2610.htm"
                                       title="搭建WAMP环境" target="_blank">搭建WAMP环境</a>
                                    <span class="text-muted">dcj3sjt126com</span>
<a class="tag" taget="_blank" href="/search/wamp/1.htm">wamp</a>
                                    <div>这里先解释一下WAMP是什么意思。W:windows,A:Apache,M:MYSQL,P:PHP。也就是说本文说明的是在windows系统下搭建以apache做服务器、MYSQL为数据库的PHP开发环境。  
    工欲善其事,必须先利其器。因为笔者的系统是WinXP,所以下文指的系统均为此系统。笔者所使用的Apache版本为apache_2.2.11-</div>
                                </li>
                                <li><a href="/article/2737.htm"
                                       title="yii2 使用raw http request" target="_blank">yii2 使用raw http request</a>
                                    <span class="text-muted">dcj3sjt126com</span>
<a class="tag" taget="_blank" href="/search/http/1.htm">http</a>
                                    <div>Parses a raw HTTP request using yii\helpers\Json::decode() 
  
To enable parsing for JSON requests you can configure yii\web\Request::$parsers using this class: 
'request' =&g</div>
                                </li>
                                <li><a href="/article/2864.htm"
                                       title="Quartz-1.8.6 理论部分" target="_blank">Quartz-1.8.6 理论部分</a>
                                    <span class="text-muted">eksliang</span>
<a class="tag" taget="_blank" href="/search/quartz/1.htm">quartz</a>
                                    <div>转载请出自出处:http://eksliang.iteye.com/blog/2207691 一.概述 
基于Quartz-1.8.6进行学习,因为Quartz2.0以后的API发生的非常大的变化,统一采用了build模式进行构建; 
什么是quartz? 
  
答:简单的说他是一个开源的java作业调度框架,为在 Java 应用程序中进行作业调度提供了简单却强大的机制。并且还能和Sp</div>
                                </li>
                                <li><a href="/article/2991.htm"
                                       title="什么是POJO?" target="_blank">什么是POJO?</a>
                                    <span class="text-muted">gupeng_ie</span>
<a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/POJO/1.htm">POJO</a><a class="tag" taget="_blank" href="/search/%E6%A1%86%E6%9E%B6/1.htm">框架</a><a class="tag" taget="_blank" href="/search/Hibernate/1.htm">Hibernate</a>
                                    <div>POJO--Plain Old Java Objects(简单的java对象) 
  
POJO是一个简单的、正规Java对象,它不包含业务逻辑处理或持久化逻辑等,也不是JavaBean、EntityBean等,不具有任何特殊角色和不继承或不实现任何其它Java框架的类或接口。 
  
POJO对象有时也被称为Data对象,大量应用于表现现实中的对象。如果项目中使用了Hiber</div>
                                </li>
                                <li><a href="/article/3118.htm"
                                       title="jQuery网站顶部定时折叠广告" target="_blank">jQuery网站顶部定时折叠广告</a>
                                    <span class="text-muted">ini</span>
<a class="tag" taget="_blank" href="/search/JavaScript/1.htm">JavaScript</a><a class="tag" taget="_blank" href="/search/html/1.htm">html</a><a class="tag" taget="_blank" href="/search/jquery/1.htm">jquery</a><a class="tag" taget="_blank" href="/search/Web/1.htm">Web</a><a class="tag" taget="_blank" href="/search/css/1.htm">css</a>
                                    <div>效果体验:http://hovertree.com/texiao/jquery/4.htmHTML文件代码: 
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>网页顶部定时收起广告jQuery特效 - HoverTree<</div>
                                </li>
                                <li><a href="/article/3245.htm"
                                       title="Spring boot内嵌的tomcat启动失败" target="_blank">Spring boot内嵌的tomcat启动失败</a>
                                    <span class="text-muted">kane_xie</span>
<a class="tag" taget="_blank" href="/search/spring+boot/1.htm">spring boot</a>
                                    <div>根据这篇guide创建了一个简单的spring boot应用,能运行且成功的访问。但移植到现有项目(基于hbase)中的时候,却报出以下错误: 
  
  
SEVERE: A child container failed during start
java.util.concurrent.ExecutionException: org.apache.catalina.Lif</div>
                                </li>
                                <li><a href="/article/3372.htm"
                                       title="leetcode: sort list" target="_blank">leetcode: sort list</a>
                                    <span class="text-muted">michelle_0916</span>
<a class="tag" taget="_blank" href="/search/Algorithm/1.htm">Algorithm</a><a class="tag" taget="_blank" href="/search/linked+list/1.htm">linked list</a><a class="tag" taget="_blank" href="/search/sort/1.htm">sort</a>
                                    <div>Sort a linked list in O(n log n) time using constant space complexity. 
====analysis======= 
mergeSort for singly-linked list  
====code=======      /** 
 * Definition for sin</div>
                                </li>
                                <li><a href="/article/3499.htm"
                                       title="nginx的安装与配置,中途遇到问题的解决" target="_blank">nginx的安装与配置,中途遇到问题的解决</a>
                                    <span class="text-muted">qifeifei</span>
<a class="tag" taget="_blank" href="/search/nginx/1.htm">nginx</a>
                                    <div>我使用的是ubuntu13.04系统,在安装nginx的时候遇到如下几个问题,然后找思路解决的,nginx 的下载与安装 
  
wget http://nginx.org/download/nginx-1.0.11.tar.gz
tar zxvf nginx-1.0.11.tar.gz
./configure
make
make install 
  
安装的时候出现</div>
                                </li>
                                <li><a href="/article/3626.htm"
                                       title="用枚举来处理java自定义异常" target="_blank">用枚举来处理java自定义异常</a>
                                    <span class="text-muted">tcrct</span>
<a class="tag" taget="_blank" href="/search/java/1.htm">java</a><a class="tag" taget="_blank" href="/search/enum/1.htm">enum</a><a class="tag" taget="_blank" href="/search/exception/1.htm">exception</a>
                                    <div>在系统开发过程中,总少不免要自己处理一些异常信息,然后将异常信息变成友好的提示返回到客户端的这样一个过程,之前都是new一个自定义的异常,当然这个所谓的自定义异常也是继承RuntimeException的,但这样往往会造成异常信息说明不一致的情况,所以就想到了用枚举来解决的办法。 
 
1,先创建一个接口,里面有两个方法,一个是getCode, 一个是getMessage 
 

public </div>
                                </li>
                                <li><a href="/article/3753.htm"
                                       title="erlang supervisor分析" target="_blank">erlang supervisor分析</a>
                                    <span class="text-muted">wudixiaotie</span>
<a class="tag" taget="_blank" href="/search/erlang/1.htm">erlang</a>
                                    <div>当我们给supervisor指定需要创建的子进程的时候,会指定M,F,A,如果是simple_one_for_one的策略的话,启动子进程的方式是supervisor:start_child(SupName, OtherArgs),这种方式可以根据调用者的需求传不同的参数给需要启动的子进程的方法。和最初的参数合并成一个数组,A ++ OtherArgs。那么这个时候就有个问题了,既然参数不一致,那</div>
                                </li>
                </ul>
            </div>
        </div>
    </div>

<div>
    <div class="container">
        <div class="indexes">
            <strong>按字母分类:</strong>
            <a href="/tags/A/1.htm" target="_blank">A</a><a href="/tags/B/1.htm" target="_blank">B</a><a href="/tags/C/1.htm" target="_blank">C</a><a
                href="/tags/D/1.htm" target="_blank">D</a><a href="/tags/E/1.htm" target="_blank">E</a><a href="/tags/F/1.htm" target="_blank">F</a><a
                href="/tags/G/1.htm" target="_blank">G</a><a href="/tags/H/1.htm" target="_blank">H</a><a href="/tags/I/1.htm" target="_blank">I</a><a
                href="/tags/J/1.htm" target="_blank">J</a><a href="/tags/K/1.htm" target="_blank">K</a><a href="/tags/L/1.htm" target="_blank">L</a><a
                href="/tags/M/1.htm" target="_blank">M</a><a href="/tags/N/1.htm" target="_blank">N</a><a href="/tags/O/1.htm" target="_blank">O</a><a
                href="/tags/P/1.htm" target="_blank">P</a><a href="/tags/Q/1.htm" target="_blank">Q</a><a href="/tags/R/1.htm" target="_blank">R</a><a
                href="/tags/S/1.htm" target="_blank">S</a><a href="/tags/T/1.htm" target="_blank">T</a><a href="/tags/U/1.htm" target="_blank">U</a><a
                href="/tags/V/1.htm" target="_blank">V</a><a href="/tags/W/1.htm" target="_blank">W</a><a href="/tags/X/1.htm" target="_blank">X</a><a
                href="/tags/Y/1.htm" target="_blank">Y</a><a href="/tags/Z/1.htm" target="_blank">Z</a><a href="/tags/0/1.htm" target="_blank">其他</a>
        </div>
    </div>
</div>
<footer id="footer" class="mb30 mt30">
    <div class="container">
        <div class="footBglm">
            <a target="_blank" href="/">首页</a> -
            <a target="_blank" href="/custom/about.htm">关于我们</a> -
            <a target="_blank" href="/search/Java/1.htm">站内搜索</a> -
            <a target="_blank" href="/sitemap.txt">Sitemap</a> -
            <a target="_blank" href="/custom/delete.htm">侵权投诉</a>
        </div>
        <div class="copyright">版权所有 IT知识库 CopyRight © 2000-2050 E-COM-NET.COM , All Rights Reserved.
<!--            <a href="https://beian.miit.gov.cn/" rel="nofollow" target="_blank">京ICP备09083238号</a><br>-->
        </div>
    </div>
</footer>
<!-- 代码高亮 -->
<script type="text/javascript" src="/static/syntaxhighlighter/scripts/shCore.js"></script>
<script type="text/javascript" src="/static/syntaxhighlighter/scripts/shLegacy.js"></script>
<script type="text/javascript" src="/static/syntaxhighlighter/scripts/shAutoloader.js"></script>
<link type="text/css" rel="stylesheet" href="/static/syntaxhighlighter/styles/shCoreDefault.css"/>
<script type="text/javascript" src="/static/syntaxhighlighter/src/my_start_1.js"></script>





</body>

</html>